Files
youlbot/services/db/mysql_service.py
T
shinalok 0b50444e43 IDEA-2/1/5/7: 스마트 알림, 대화 기반 RAG, CRAG, 파라미터 자동 튜닝
- IDEA-2 스마트 알림: td_reminders 테이블, set_reminder/list_reminders 도구,
  SchedulerService(asyncio 60초 루프, D-7/D-1/D-0 Telegram push),
  FastAPI lifespan 연동, GET /reminders/{user_id} 엔드포인트

- IDEA-1 대화 기반 RAG: IngestionService.store_text() 추가,
  AgentService._maybe_index_conversation() — 응답 후 LLM 판단 → Qdrant 저장
  (CONV_RAG_ENABLED=true 활성화, background task로 응답 속도 무관)

- IDEA-5 CRAG: AgentState에 crag_fallback_used 플래그 추가,
  crag_check LangGraph 노드 — search_documents 결과 없으면 web_search 자동 주입,
  route_after_crag으로 fallback 1회 루프 제어 (CRAG_ENABLED=true 활성화)

- IDEA-7 RAG Auto-Eval: eval/auto_tune.py — API 서버 없이 파라미터 조합별
  context_precision/recall 비교, 최적 설정 추천

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 10:04:05 +09:00

147 lines
5.2 KiB
Python

from __future__ import annotations
import threading
from typing import Any
class DatabaseService:
"""MySQL 연결을 캡슐화하는 서비스. 미설정 시 graceful skip.
스레드별 독립 연결(thread-local)을 사용해 LangGraph ToolNode의
스레드 풀 실행과 pymysql 비안전성 문제를 해결한다.
"""
def __init__(
self,
host: str,
port: int,
db: str,
user: str,
password: str,
):
self._config = dict(host=host, port=port, db=db, user=user, passwd=password)
self._local = threading.local()
# ── DB 연결 ────────────────────────────────────────────────────────
def _get_conn(self):
if not self._config["user"]:
return None
import pymysql
conn = getattr(self._local, "conn", None)
if conn is None:
try:
self._local.conn = pymysql.connect(**self._config)
except Exception as e:
print(f"[DB] 연결 실패: {e}")
return None
else:
try:
conn.ping(reconnect=True)
except Exception:
try:
self._local.conn = pymysql.connect(**self._config)
except Exception as e:
print(f"[DB] 재연결 실패: {e}")
return None
return self._local.conn
def connect(self) -> None:
self._get_conn()
def execute(self, sql: str, params: tuple = ()) -> list[dict[str, Any]]:
conn = self._get_conn()
if conn is None:
return []
cursor = conn.cursor()
cursor.execute(sql, params)
columns = [d[0] for d in cursor.description or []]
return [dict(zip(columns, row)) for row in cursor.fetchall()]
def execute_write(self, sql: str, params: tuple = ()) -> int:
conn = self._get_conn()
if conn is None:
return 0
cursor = conn.cursor()
cursor.execute(sql, params)
conn.commit()
return cursor.lastrowid
def init_schema(self) -> None:
conn = self._get_conn()
if conn is None:
return
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS td_conversations (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(50) NOT NULL DEFAULT 'default',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS td_messages (
id INT AUTO_INCREMENT PRIMARY KEY,
conversation_id INT NOT NULL,
role VARCHAR(20) NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (conversation_id) REFERENCES td_conversations(id)
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS td_user_profile (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(50) NOT NULL DEFAULT 'default',
key_name VARCHAR(100) NOT NULL,
value TEXT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_user_key (user_id, key_name)
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS td_feedback (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(50) NOT NULL DEFAULT 'default',
message TEXT,
response TEXT,
rating TINYINT,
langsmith_run_id VARCHAR(100),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS td_reminders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(50) NOT NULL,
remind_date DATE NOT NULL,
message TEXT NOT NULL,
sent_d0 TINYINT(1) NOT NULL DEFAULT 0,
sent_d1 TINYINT(1) NOT NULL DEFAULT 0,
sent_d7 TINYINT(1) NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
self._migrate_schema(conn)
def _migrate_schema(self, conn) -> None:
cursor = conn.cursor()
for sql in [
"ALTER TABLE td_conversations ADD COLUMN user_id VARCHAR(50) NOT NULL DEFAULT 'default'",
"ALTER TABLE td_user_profile ADD COLUMN user_id VARCHAR(50) NOT NULL DEFAULT 'default'",
"ALTER TABLE td_user_profile DROP INDEX key_name",
"ALTER TABLE td_user_profile ADD UNIQUE KEY uq_user_key (user_id, key_name)",
]:
try:
cursor.execute(sql)
conn.commit()
except Exception:
pass
def close(self) -> None:
conn = getattr(self._local, "conn", None)
if conn:
conn.close()
self._local.conn = None