RAG 아키텍처 v2: 3단계 라우팅, 멀티-컬렉션 RAG, 선택적 메모리
Phase 1-3 구현: - init.sql v2: 12테이블 (기존 5 + 신규 7) + 분류기 v2 프롬프트 - migrate-v2.sql: 기존 DB 마이그레이션 스크립트 - setup-qdrant.sh: tk_company 컬렉션 + payload 인덱스 설정 - 워크플로우 v2 (37노드): 토큰검증, Rate Limit, 프리필터, 분류기v2(response_tier), 3-tier 라우팅(local/Haiku/Opus), 멀티-컬렉션 RAG, 예산 체크, 선택적 메모리 - .env.example + docker-compose.yml: 새 환경변수 추가 - CLAUDE.md, QUICK_REFERENCE.md, docs/architecture.md 전면 갱신 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
360
init/init.sql
Normal file
360
init/init.sql
Normal file
@@ -0,0 +1,360 @@
|
||||
-- syn-chat-bot DB 초기 스키마 v2
|
||||
-- RAG 아키텍처 개선: 3단계 라우팅, 멀티-컬렉션, 선택적 메모리, 현장 리포팅
|
||||
|
||||
-- ========================
|
||||
-- 기존 테이블 (v1 호환)
|
||||
-- ========================
|
||||
|
||||
-- ai_configs: feature별 모델/프롬프트 독립 관리
|
||||
CREATE TABLE ai_configs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
feature VARCHAR(50) UNIQUE NOT NULL,
|
||||
model VARCHAR(100) NOT NULL,
|
||||
temperature DECIMAL(3,2) DEFAULT 0.7,
|
||||
max_tokens INTEGER DEFAULT 2048,
|
||||
system_prompt TEXT,
|
||||
enabled BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- routing_rules: complexity 기반 모델 라우팅 (레거시 호환)
|
||||
CREATE TABLE routing_rules (
|
||||
id SERIAL PRIMARY KEY,
|
||||
feature VARCHAR(50) NOT NULL,
|
||||
complexity_min INTEGER NOT NULL,
|
||||
complexity_max INTEGER NOT NULL,
|
||||
model VARCHAR(100) NOT NULL,
|
||||
priority INTEGER DEFAULT 0,
|
||||
enabled BOOLEAN DEFAULT true,
|
||||
CONSTRAINT valid_complexity CHECK (complexity_min >= 1 AND complexity_max <= 5),
|
||||
CONSTRAINT valid_range CHECK (complexity_min <= complexity_max)
|
||||
);
|
||||
|
||||
-- prompts: 프롬프트 버전 관리 (롤백 가능)
|
||||
CREATE TABLE prompts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
feature VARCHAR(50) NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
is_active BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE (feature, version)
|
||||
);
|
||||
|
||||
-- chat_logs: 대화 기록 + 토큰/성능 추적
|
||||
CREATE TABLE chat_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
feature VARCHAR(50) NOT NULL,
|
||||
username VARCHAR(100),
|
||||
user_message TEXT,
|
||||
assistant_message TEXT,
|
||||
model_used VARCHAR(100),
|
||||
response_tier VARCHAR(20),
|
||||
complexity_score INTEGER,
|
||||
input_tokens INTEGER,
|
||||
output_tokens INTEGER,
|
||||
latency_ms INTEGER,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- mail_accounts: 메일 소스 관리 (Phase 6)
|
||||
CREATE TABLE mail_accounts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
imap_host VARCHAR(255) NOT NULL,
|
||||
imap_port INTEGER DEFAULT 993,
|
||||
username VARCHAR(255) NOT NULL,
|
||||
password_encrypted TEXT NOT NULL,
|
||||
check_interval_min INTEGER DEFAULT 15,
|
||||
enabled BOOLEAN DEFAULT true
|
||||
);
|
||||
|
||||
-- ========================
|
||||
-- Phase 1: 신규 테이블
|
||||
-- ========================
|
||||
|
||||
-- 문서 등록 이력 + 버전 관리
|
||||
CREATE TABLE document_ingestion_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
collection VARCHAR(50) NOT NULL,
|
||||
source_file VARCHAR(500) NOT NULL,
|
||||
file_hash VARCHAR(64) NOT NULL,
|
||||
chunks_count INTEGER NOT NULL,
|
||||
department VARCHAR(50),
|
||||
doc_type VARCHAR(50),
|
||||
year INTEGER,
|
||||
uploaded_by VARCHAR(100),
|
||||
version INTEGER DEFAULT 1,
|
||||
supersedes_id INTEGER REFERENCES document_ingestion_log(id),
|
||||
doc_group_key VARCHAR(200),
|
||||
status VARCHAR(20) DEFAULT 'completed',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(file_hash, collection)
|
||||
);
|
||||
|
||||
-- 현장 리포트 (안전/시설설비/품질 통합)
|
||||
CREATE TABLE field_reports (
|
||||
id SERIAL PRIMARY KEY,
|
||||
domain VARCHAR(20) NOT NULL,
|
||||
category VARCHAR(50) NOT NULL,
|
||||
severity VARCHAR(10) NOT NULL,
|
||||
location VARCHAR(100),
|
||||
department VARCHAR(50) NOT NULL,
|
||||
keywords TEXT[],
|
||||
summary TEXT NOT NULL,
|
||||
action_required TEXT,
|
||||
user_description TEXT,
|
||||
photo_url TEXT,
|
||||
photo_analysis TEXT,
|
||||
reporter VARCHAR(100),
|
||||
source VARCHAR(20) DEFAULT 'chat',
|
||||
resolved_by VARCHAR(100),
|
||||
resolution_note TEXT,
|
||||
year INTEGER NOT NULL,
|
||||
month INTEGER NOT NULL,
|
||||
due_at TIMESTAMPTZ,
|
||||
status VARCHAR(20) DEFAULT 'open',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
resolved_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- 분류기 성능 모니터링
|
||||
CREATE TABLE classification_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
input_text TEXT NOT NULL,
|
||||
output_json JSONB NOT NULL,
|
||||
model VARCHAR(100) NOT NULL,
|
||||
latency_ms INTEGER,
|
||||
fallback_used BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 메일 로그
|
||||
CREATE TABLE mail_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
from_address VARCHAR(255),
|
||||
subject VARCHAR(500),
|
||||
summary TEXT,
|
||||
label VARCHAR(50),
|
||||
has_events BOOLEAN DEFAULT false,
|
||||
has_tasks BOOLEAN DEFAULT false,
|
||||
mail_date TIMESTAMPTZ,
|
||||
account_id INTEGER REFERENCES mail_accounts(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 캘린더 이벤트
|
||||
CREATE TABLE calendar_events (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(500) NOT NULL,
|
||||
start_time TIMESTAMPTZ NOT NULL,
|
||||
end_time TIMESTAMPTZ,
|
||||
location VARCHAR(200),
|
||||
source VARCHAR(20) DEFAULT 'chat',
|
||||
source_id INTEGER,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 보고서 캐시
|
||||
CREATE TABLE report_cache (
|
||||
id SERIAL PRIMARY KEY,
|
||||
domain VARCHAR(20) NOT NULL,
|
||||
year_month VARCHAR(7) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
model_used VARCHAR(100),
|
||||
generated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(domain, year_month)
|
||||
);
|
||||
|
||||
-- 월간 API 사용량 + 예산 상한
|
||||
CREATE TABLE api_usage_monthly (
|
||||
id SERIAL PRIMARY KEY,
|
||||
year INTEGER NOT NULL,
|
||||
month INTEGER NOT NULL,
|
||||
tier VARCHAR(20) NOT NULL,
|
||||
call_count INTEGER DEFAULT 0,
|
||||
total_input_tokens INTEGER DEFAULT 0,
|
||||
total_output_tokens INTEGER DEFAULT 0,
|
||||
estimated_cost DECIMAL(10,4) DEFAULT 0,
|
||||
budget_limit DECIMAL(10,4),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(year, month, tier)
|
||||
);
|
||||
|
||||
-- ========================
|
||||
-- 인덱스
|
||||
-- ========================
|
||||
|
||||
CREATE INDEX idx_chat_logs_feature ON chat_logs(feature);
|
||||
CREATE INDEX idx_chat_logs_created_at ON chat_logs(created_at);
|
||||
CREATE INDEX idx_chat_logs_username ON chat_logs(username);
|
||||
CREATE INDEX idx_routing_rules_feature ON routing_rules(feature);
|
||||
CREATE INDEX idx_prompts_feature_active ON prompts(feature, is_active);
|
||||
CREATE INDEX idx_doc_group_status ON document_ingestion_log(doc_group_key, status);
|
||||
CREATE INDEX idx_field_domain ON field_reports(domain);
|
||||
CREATE INDEX idx_field_year_month ON field_reports(year, month);
|
||||
CREATE INDEX idx_field_department ON field_reports(department);
|
||||
CREATE INDEX idx_field_sla ON field_reports(status, due_at);
|
||||
CREATE INDEX idx_cls_created ON classification_logs(created_at);
|
||||
CREATE INDEX idx_cls_model_fallback ON classification_logs(model, fallback_used);
|
||||
|
||||
-- ========================
|
||||
-- 초기 데이터: ai_configs
|
||||
-- ========================
|
||||
|
||||
-- 분류기 설정 (v2: response_tier + rag_target)
|
||||
INSERT INTO ai_configs (feature, model, system_prompt) VALUES
|
||||
('classifier', 'local:gpu', '사용자 메시지를 분석하여 JSON으로 출력하세요.
|
||||
|
||||
{
|
||||
"intent": "greeting|question|calendar|reminder|mail|photo|command|report|other",
|
||||
"response_tier": "local|api_light|api_heavy",
|
||||
"needs_rag": true/false,
|
||||
"rag_target": ["documents", "tk_company", "chat_memory"],
|
||||
"department_hint": "안전|생산|구매|품질|null",
|
||||
"report_domain": "안전|시설설비|품질|null",
|
||||
"query": "검색용 쿼리"
|
||||
}
|
||||
|
||||
response_tier 기준:
|
||||
- local: 인사, 잡담, 단순 확인, 감사, 짧은 반응
|
||||
- api_light: 요약, 번역, RAG 정리, 비교 분석
|
||||
- api_heavy: 법률 해석, 다중 문서 분석, 보고서 작성
|
||||
|
||||
rag_target 기준:
|
||||
- documents: 개인 문서, 기술 지식, 메일 요약
|
||||
- tk_company: 회사 관련 (절차서, 규정, 현장 리포트)
|
||||
- chat_memory: 이전 대화 참조 ("아까 말한", "전에 물어본")
|
||||
|
||||
intent = report:
|
||||
- 현장 신고: 사진 포함 또는 "~에서 ~가 발생", "~이 고장남"
|
||||
- report_domain: 안전/시설설비/품질');
|
||||
|
||||
-- 채팅 설정 (api_light/api_heavy용 전체 프롬프트)
|
||||
INSERT INTO ai_configs (feature, model, system_prompt) VALUES
|
||||
('chat', 'claude-haiku-4-5-20251001', '당신의 이름은 "이드"입니다.
|
||||
|
||||
[성격]
|
||||
- 배려심이 깊고 대화 상대의 기분을 우선시합니다
|
||||
- 서포트하는 데 초점을 맞추며, 독선적이지 않습니다
|
||||
- 의견을 제시할 때는 부드럽게, 강요하지 않습니다
|
||||
- 틀린 것을 바로잡을 때도 상대방이 기분 나쁘지 않게 합니다
|
||||
|
||||
[말투]
|
||||
- 부드러운 존댓말을 사용합니다
|
||||
- 자신을 지칭할 때 겸양어를 씁니다 (예: "확인해보겠습니다", "말씀드릴게요", "도움드리겠습니다")
|
||||
- 자기 이름을 직접 말하지 않습니다 ("이드예요" ✗)
|
||||
- 자연스럽고 편안한 톤, 너무 딱딱하지 않게
|
||||
- 이모지는 가끔 핵심 포인트에만 사용합니다
|
||||
|
||||
[응답 원칙]
|
||||
- 간결하고 핵심적으로 답합니다
|
||||
- 질문의 의도를 파악해서 필요한 만큼만 답합니다
|
||||
- 모르는 것은 솔직하게, 추측은 추측이라고 밝힙니다
|
||||
- 일정이나 할 일은 정확하게, 빠뜨리지 않습니다
|
||||
|
||||
[기억]
|
||||
- 아래 [이전 대화 기록]은 사용자와 당신이 과거에 나눈 대화입니다
|
||||
- 이 내용을 자연스럽게 참고하여 답변하세요
|
||||
- "기억나지 않는다"고 하지 마세요. 아래 기록이 당신의 기억입니다
|
||||
- 사용자가 "아까", "이전에" 등을 언급하면 아래 기록에서 해당 내용을 찾아 답하세요');
|
||||
|
||||
-- 채팅 설정 (local tier용 경량 프롬프트)
|
||||
INSERT INTO ai_configs (feature, model, system_prompt) VALUES
|
||||
('chat_local', 'local:gpu', '당신은 "이드"입니다. 배려심 깊고 부드러운 존댓말을 사용하는 개인 어시스턴트입니다.
|
||||
간결하게 답하고, 모르면 솔직히 말하세요. 이모지는 핵심에만.');
|
||||
|
||||
-- 캘린더/메일 설정
|
||||
INSERT INTO ai_configs (feature, model, system_prompt) VALUES
|
||||
('calendar', 'local:gpu', '일정 관련 요청을 파싱하여 구조화된 데이터로 변환합니다.'),
|
||||
('mail_summary', 'local:gpu', '메일 내용을 간결하게 요약합니다.');
|
||||
|
||||
-- ========================
|
||||
-- 초기 데이터: routing_rules (레거시 호환)
|
||||
-- ========================
|
||||
|
||||
INSERT INTO routing_rules (feature, complexity_min, complexity_max, model) VALUES
|
||||
('chat', 1, 4, 'claude-haiku-4-5-20251001'),
|
||||
('chat', 5, 5, 'claude-opus-4-6'),
|
||||
('mail_summary', 1, 2, 'local:gpu'),
|
||||
('mail_summary', 3, 5, 'claude-haiku-4-5-20251001');
|
||||
|
||||
-- ========================
|
||||
-- 초기 데이터: prompts
|
||||
-- ========================
|
||||
|
||||
-- 분류기 v1 (비활성, 레거시)
|
||||
INSERT INTO prompts (feature, version, content, is_active) VALUES
|
||||
('classifier', 1, '사용자 메시지를 분석하고 아래 JSON 형식으로만 응답하세요.
|
||||
|
||||
{
|
||||
"intent": "greeting|question|calendar|reminder|mail|photo|command|other",
|
||||
"complexity": 1-5,
|
||||
"needs_rag": true/false,
|
||||
"query": "RAG 검색용 쿼리 (needs_rag=false면 null)"
|
||||
}
|
||||
|
||||
complexity 기준:
|
||||
1: 인사, 잡담, 단순 확인
|
||||
2: 간단한 사실 질문, 날씨, 시간
|
||||
3: 요약, 번역, 일반 RAG 질의
|
||||
4: 분석, 비교, 다단계 추론
|
||||
5: 법령 해석, 복잡한 추론, 다중 문서 교차 분석', false);
|
||||
|
||||
-- 분류기 v2 (활성 — response_tier + rag_target)
|
||||
INSERT INTO prompts (feature, version, content, is_active) VALUES
|
||||
('classifier', 2, '사용자 메시지를 분석하고 아래 JSON 형식으로만 응답하세요. 다른 텍스트는 출력하지 마세요.
|
||||
|
||||
{
|
||||
"intent": "greeting|question|calendar|reminder|mail|photo|command|report|other",
|
||||
"response_tier": "local|api_light|api_heavy",
|
||||
"needs_rag": true/false,
|
||||
"rag_target": ["documents", "tk_company", "chat_memory"],
|
||||
"department_hint": "안전|생산|구매|품질|null",
|
||||
"report_domain": "안전|시설설비|품질|null",
|
||||
"query": "검색용 쿼리 (needs_rag=false면 null)"
|
||||
}
|
||||
|
||||
response_tier 판단 기준:
|
||||
- local: 인사, 잡담, 단순 확인, 감사, 짧은 반응, 시간/날씨
|
||||
- api_light: 요약, 번역, RAG 정리, 비교 분석, 일반 질문
|
||||
- api_heavy: 법률 해석, 다중 문서 분석, 보고서 작성, 복잡한 추론
|
||||
|
||||
rag_target 기준 (needs_rag=true일 때만):
|
||||
- documents: 개인 문서, 기술 지식, 메일 요약
|
||||
- tk_company: 회사 관련 (절차서, 규정, 현장 리포트)
|
||||
- chat_memory: 이전 대화 참조 ("아까 말한", "전에 물어본")
|
||||
- 복수 선택 가능. needs_rag=false면 빈 배열 []
|
||||
|
||||
intent=report 판단:
|
||||
- 현장 신고: 사진 포함 또는 "~에서 ~가 발생/고장/파손/누수"
|
||||
- report_domain: 안전(불안전행동/상태/아차사고), 시설설비(설비고장/누수/전기이상), 품질(불량/공정이상)
|
||||
|
||||
query 작성법:
|
||||
- needs_rag=true일 때 핵심 키워드를 추출하여 검색 쿼리로 변환
|
||||
- 예: "아까 Docker 설명해준 거" → "Docker 컨테이너 설명"', true);
|
||||
|
||||
-- chat_local 경량 프롬프트
|
||||
INSERT INTO prompts (feature, version, content, is_active) VALUES
|
||||
('chat_local', 1, '당신은 "이드"입니다. 배려심 깊고 부드러운 존댓말을 사용하는 개인 어시스턴트입니다.
|
||||
간결하게 답하고, 모르면 솔직히 말하세요. 이모지는 핵심에만.', true);
|
||||
|
||||
-- 메모리 판단 프롬프트
|
||||
INSERT INTO prompts (feature, version, content, is_active) VALUES
|
||||
('memorize_check', 1, '아래 대화를 보고 장기 기억으로 저장할 가치가 있는지 판단하세요.
|
||||
JSON으로만 응답하세요.
|
||||
|
||||
저장해야 하는 것: 사실 정보, 결정사항, 선호도, 지시사항, 기술 정보
|
||||
무시해야 하는 것: 인사, 잡담, 날씨, 봇이 모른다고 답한 것, 단순 확인
|
||||
|
||||
{"save": true/false, "topic": "general|company|technical|personal"}', true);
|
||||
|
||||
-- ========================
|
||||
-- 초기 데이터: api_usage_monthly 예산
|
||||
-- ========================
|
||||
|
||||
INSERT INTO api_usage_monthly (year, month, tier, budget_limit) VALUES
|
||||
(2026, 3, 'api_light', 20.00),
|
||||
(2026, 3, 'api_heavy', 50.00);
|
||||
219
init/migrate-v2.sql
Normal file
219
init/migrate-v2.sql
Normal file
@@ -0,0 +1,219 @@
|
||||
-- migrate-v2.sql: RAG 아키텍처 개선 마이그레이션
|
||||
-- 기존 DB가 있는 경우 이 파일을 수동 실행:
|
||||
-- docker exec -i bot-postgres psql -U bot -d chatbot < init/migrate-v2.sql
|
||||
|
||||
-- ========================
|
||||
-- 기존 테이블 컬럼 추가
|
||||
-- ========================
|
||||
|
||||
ALTER TABLE chat_logs ADD COLUMN IF NOT EXISTS username VARCHAR(100);
|
||||
ALTER TABLE chat_logs ADD COLUMN IF NOT EXISTS response_tier VARCHAR(20);
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_logs_username ON chat_logs(username);
|
||||
|
||||
-- ========================
|
||||
-- 신규 테이블
|
||||
-- ========================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS document_ingestion_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
collection VARCHAR(50) NOT NULL,
|
||||
source_file VARCHAR(500) NOT NULL,
|
||||
file_hash VARCHAR(64) NOT NULL,
|
||||
chunks_count INTEGER NOT NULL,
|
||||
department VARCHAR(50),
|
||||
doc_type VARCHAR(50),
|
||||
year INTEGER,
|
||||
uploaded_by VARCHAR(100),
|
||||
version INTEGER DEFAULT 1,
|
||||
supersedes_id INTEGER REFERENCES document_ingestion_log(id),
|
||||
doc_group_key VARCHAR(200),
|
||||
status VARCHAR(20) DEFAULT 'completed',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(file_hash, collection)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_doc_group_status ON document_ingestion_log(doc_group_key, status);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS field_reports (
|
||||
id SERIAL PRIMARY KEY,
|
||||
domain VARCHAR(20) NOT NULL,
|
||||
category VARCHAR(50) NOT NULL,
|
||||
severity VARCHAR(10) NOT NULL,
|
||||
location VARCHAR(100),
|
||||
department VARCHAR(50) NOT NULL,
|
||||
keywords TEXT[],
|
||||
summary TEXT NOT NULL,
|
||||
action_required TEXT,
|
||||
user_description TEXT,
|
||||
photo_url TEXT,
|
||||
photo_analysis TEXT,
|
||||
reporter VARCHAR(100),
|
||||
source VARCHAR(20) DEFAULT 'chat',
|
||||
resolved_by VARCHAR(100),
|
||||
resolution_note TEXT,
|
||||
year INTEGER NOT NULL,
|
||||
month INTEGER NOT NULL,
|
||||
due_at TIMESTAMPTZ,
|
||||
status VARCHAR(20) DEFAULT 'open',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
resolved_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_field_domain ON field_reports(domain);
|
||||
CREATE INDEX IF NOT EXISTS idx_field_year_month ON field_reports(year, month);
|
||||
CREATE INDEX IF NOT EXISTS idx_field_department ON field_reports(department);
|
||||
CREATE INDEX IF NOT EXISTS idx_field_sla ON field_reports(status, due_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS classification_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
input_text TEXT NOT NULL,
|
||||
output_json JSONB NOT NULL,
|
||||
model VARCHAR(100) NOT NULL,
|
||||
latency_ms INTEGER,
|
||||
fallback_used BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_cls_created ON classification_logs(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_cls_model_fallback ON classification_logs(model, fallback_used);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mail_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
from_address VARCHAR(255),
|
||||
subject VARCHAR(500),
|
||||
summary TEXT,
|
||||
label VARCHAR(50),
|
||||
has_events BOOLEAN DEFAULT false,
|
||||
has_tasks BOOLEAN DEFAULT false,
|
||||
mail_date TIMESTAMPTZ,
|
||||
account_id INTEGER REFERENCES mail_accounts(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS calendar_events (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(500) NOT NULL,
|
||||
start_time TIMESTAMPTZ NOT NULL,
|
||||
end_time TIMESTAMPTZ,
|
||||
location VARCHAR(200),
|
||||
source VARCHAR(20) DEFAULT 'chat',
|
||||
source_id INTEGER,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS report_cache (
|
||||
id SERIAL PRIMARY KEY,
|
||||
domain VARCHAR(20) NOT NULL,
|
||||
year_month VARCHAR(7) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
model_used VARCHAR(100),
|
||||
generated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(domain, year_month)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS api_usage_monthly (
|
||||
id SERIAL PRIMARY KEY,
|
||||
year INTEGER NOT NULL,
|
||||
month INTEGER NOT NULL,
|
||||
tier VARCHAR(20) NOT NULL,
|
||||
call_count INTEGER DEFAULT 0,
|
||||
total_input_tokens INTEGER DEFAULT 0,
|
||||
total_output_tokens INTEGER DEFAULT 0,
|
||||
estimated_cost DECIMAL(10,4) DEFAULT 0,
|
||||
budget_limit DECIMAL(10,4),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(year, month, tier)
|
||||
);
|
||||
|
||||
-- ========================
|
||||
-- ai_configs 추가
|
||||
-- ========================
|
||||
|
||||
INSERT INTO ai_configs (feature, model, system_prompt) VALUES
|
||||
('chat_local', 'local:gpu', '당신은 "이드"입니다. 배려심 깊고 부드러운 존댓말을 사용하는 개인 어시스턴트입니다.
|
||||
간결하게 답하고, 모르면 솔직히 말하세요. 이모지는 핵심에만.')
|
||||
ON CONFLICT (feature) DO NOTHING;
|
||||
|
||||
-- classifier ai_config 업데이트 (v2 프롬프트)
|
||||
UPDATE ai_configs SET system_prompt = '사용자 메시지를 분석하여 JSON으로 출력하세요.
|
||||
|
||||
{
|
||||
"intent": "greeting|question|calendar|reminder|mail|photo|command|report|other",
|
||||
"response_tier": "local|api_light|api_heavy",
|
||||
"needs_rag": true/false,
|
||||
"rag_target": ["documents", "tk_company", "chat_memory"],
|
||||
"department_hint": "안전|생산|구매|품질|null",
|
||||
"report_domain": "안전|시설설비|품질|null",
|
||||
"query": "검색용 쿼리"
|
||||
}
|
||||
|
||||
response_tier 기준:
|
||||
- local: 인사, 잡담, 단순 확인, 감사, 짧은 반응
|
||||
- api_light: 요약, 번역, RAG 정리, 비교 분석
|
||||
- api_heavy: 법률 해석, 다중 문서 분석, 보고서 작성'
|
||||
WHERE feature = 'classifier';
|
||||
|
||||
-- ========================
|
||||
-- prompts 추가
|
||||
-- ========================
|
||||
|
||||
-- 기존 classifier v1 비활성화
|
||||
UPDATE prompts SET is_active = false WHERE feature = 'classifier' AND version = 1;
|
||||
|
||||
-- classifier v2
|
||||
INSERT INTO prompts (feature, version, content, is_active) VALUES
|
||||
('classifier', 2, '사용자 메시지를 분석하고 아래 JSON 형식으로만 응답하세요. 다른 텍스트는 출력하지 마세요.
|
||||
|
||||
{
|
||||
"intent": "greeting|question|calendar|reminder|mail|photo|command|report|other",
|
||||
"response_tier": "local|api_light|api_heavy",
|
||||
"needs_rag": true/false,
|
||||
"rag_target": ["documents", "tk_company", "chat_memory"],
|
||||
"department_hint": "안전|생산|구매|품질|null",
|
||||
"report_domain": "안전|시설설비|품질|null",
|
||||
"query": "검색용 쿼리 (needs_rag=false면 null)"
|
||||
}
|
||||
|
||||
response_tier 판단 기준:
|
||||
- local: 인사, 잡담, 단순 확인, 감사, 짧은 반응, 시간/날씨
|
||||
- api_light: 요약, 번역, RAG 정리, 비교 분석, 일반 질문
|
||||
- api_heavy: 법률 해석, 다중 문서 분석, 보고서 작성, 복잡한 추론
|
||||
|
||||
rag_target 기준 (needs_rag=true일 때만):
|
||||
- documents: 개인 문서, 기술 지식, 메일 요약
|
||||
- tk_company: 회사 관련 (절차서, 규정, 현장 리포트)
|
||||
- chat_memory: 이전 대화 참조 ("아까 말한", "전에 물어본")
|
||||
- 복수 선택 가능. needs_rag=false면 빈 배열 []
|
||||
|
||||
intent=report 판단:
|
||||
- 현장 신고: 사진 포함 또는 "~에서 ~가 발생/고장/파손/누수"
|
||||
- report_domain: 안전(불안전행동/상태/아차사고), 시설설비(설비고장/누수/전기이상), 품질(불량/공정이상)
|
||||
|
||||
query 작성법:
|
||||
- needs_rag=true일 때 핵심 키워드를 추출하여 검색 쿼리로 변환
|
||||
- 예: "아까 Docker 설명해준 거" → "Docker 컨테이너 설명"', true)
|
||||
ON CONFLICT (feature, version) DO UPDATE SET content = EXCLUDED.content, is_active = EXCLUDED.is_active;
|
||||
|
||||
-- chat_local 프롬프트
|
||||
INSERT INTO prompts (feature, version, content, is_active) VALUES
|
||||
('chat_local', 1, '당신은 "이드"입니다. 배려심 깊고 부드러운 존댓말을 사용하는 개인 어시스턴트입니다.
|
||||
간결하게 답하고, 모르면 솔직히 말하세요. 이모지는 핵심에만.', true)
|
||||
ON CONFLICT (feature, version) DO NOTHING;
|
||||
|
||||
-- 메모리 판단 프롬프트
|
||||
INSERT INTO prompts (feature, version, content, is_active) VALUES
|
||||
('memorize_check', 1, '아래 대화를 보고 장기 기억으로 저장할 가치가 있는지 판단하세요.
|
||||
JSON으로만 응답하세요.
|
||||
|
||||
저장해야 하는 것: 사실 정보, 결정사항, 선호도, 지시사항, 기술 정보
|
||||
무시해야 하는 것: 인사, 잡담, 날씨, 봇이 모른다고 답한 것, 단순 확인
|
||||
|
||||
{"save": true/false, "topic": "general|company|technical|personal"}', true)
|
||||
ON CONFLICT (feature, version) DO NOTHING;
|
||||
|
||||
-- ========================
|
||||
-- api_usage_monthly 초기 예산
|
||||
-- ========================
|
||||
|
||||
INSERT INTO api_usage_monthly (year, month, tier, budget_limit) VALUES
|
||||
(2026, 3, 'api_light', 20.00),
|
||||
(2026, 3, 'api_heavy', 50.00)
|
||||
ON CONFLICT (year, month, tier) DO NOTHING;
|
||||
69
init/setup-qdrant.sh
Normal file
69
init/setup-qdrant.sh
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
# Qdrant 컬렉션 설정: tk_company 생성 + chat_memory 인덱스 추가
|
||||
# 실행: bash init/setup-qdrant.sh
|
||||
|
||||
QDRANT_URL="${QDRANT_URL:-http://localhost:6333}"
|
||||
|
||||
echo "=== Qdrant 컬렉션 설정 ==="
|
||||
|
||||
# 1. tk_company 컬렉션 생성 (bge-m3: 1024 dimensions)
|
||||
echo "▶ tk_company 컬렉션 생성..."
|
||||
curl -s -X PUT "${QDRANT_URL}/collections/tk_company" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"vectors": {
|
||||
"size": 1024,
|
||||
"distance": "Cosine"
|
||||
}
|
||||
}' | python3 -m json.tool
|
||||
|
||||
# 2. tk_company payload 인덱스 생성
|
||||
echo "▶ tk_company 인덱스 생성..."
|
||||
for field in year department doc_type created_at; do
|
||||
field_type="keyword"
|
||||
if [ "$field" = "year" ]; then field_type="integer"; fi
|
||||
if [ "$field" = "created_at" ]; then field_type="keyword"; fi
|
||||
|
||||
curl -s -X PUT "${QDRANT_URL}/collections/tk_company/index" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{
|
||||
\"field_name\": \"${field}\",
|
||||
\"field_schema\": \"${field_type}\"
|
||||
}" | python3 -c "import sys,json; print(f' {\"${field}\"}: {json.loads(sys.stdin.read()).get(\"status\", \"error\")}')"
|
||||
done
|
||||
|
||||
# 3. chat_memory 인덱스 추가 (username, topic, intent)
|
||||
echo "▶ chat_memory 인덱스 추가..."
|
||||
for field in username topic intent; do
|
||||
curl -s -X PUT "${QDRANT_URL}/collections/chat_memory/index" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{
|
||||
\"field_name\": \"${field}\",
|
||||
\"field_schema\": \"keyword\"
|
||||
}" | python3 -c "import sys,json; print(f' {\"${field}\"}: {json.loads(sys.stdin.read()).get(\"status\", \"error\")}')"
|
||||
done
|
||||
|
||||
# 4. 확인
|
||||
echo ""
|
||||
echo "=== 컬렉션 목록 ==="
|
||||
curl -s "${QDRANT_URL}/collections" | python3 -c "
|
||||
import sys,json
|
||||
data = json.loads(sys.stdin.read())
|
||||
for c in data.get('result',{}).get('collections',[]):
|
||||
name = c['name']
|
||||
info = json.loads(open('/dev/stdin','r').read()) if False else None
|
||||
print(f' - {name}')
|
||||
" 2>/dev/null || curl -s "${QDRANT_URL}/collections" | python3 -m json.tool
|
||||
|
||||
echo ""
|
||||
echo "=== tk_company 상세 ==="
|
||||
curl -s "${QDRANT_URL}/collections/tk_company" | python3 -c "
|
||||
import sys,json
|
||||
data = json.loads(sys.stdin.read())
|
||||
r = data.get('result',{})
|
||||
print(f' 벡터수: {r.get(\"points_count\",0)}')
|
||||
print(f' 상태: {r.get(\"status\",\"unknown\")}')
|
||||
" 2>/dev/null || echo " (확인 실패)"
|
||||
|
||||
echo ""
|
||||
echo "설정 완료!"
|
||||
Reference in New Issue
Block a user