Files
youlbot/services/db/mysql_service.py
T
shinalok 06bcdb03ac Implement Phase 4~14: LangGraph Agent, RAG pipeline, Gradio Web UI, voice interface
- Upgrade LLM to Qwen3-14B-4bit with Thinking mode (MlxChatModel as LangChain BaseChatModel)
- Add LangGraph ReAct agent with tool calling loop (search_documents, web_search, get_current_date, remember/recall_user_info)
- Add RAG pipeline: BAAI/bge-m3 embeddings + Qdrant vector store + semantic chunking (SemanticSplitter via cosine similarity)
- Replace fixed-size RecursiveCharacterTextSplitter with meaning-based SemanticSplitter (numpy only, no extra deps)
- Add Gradio Web UI (app.py): chat, document ingestion, document management tabs
- Add multi-user support (user_id isolation in DB + per-user agent cache + dropdown selector)
- Add conversation history restore from MySQL on agent init (Phase 11)
- Add UserProfileRepository for persistent user profile (remember/recall tools)
- Add thread-local DB connections to fix pymysql thread-safety with LangGraph ToolNode
- Add Phase 14 voice interface: Whisper STT (microphone → text) + macOS TTS (say -v Yuna)
- Enforce search_documents-first policy in system prompt and tool descriptions
- Update ROADMAP2.md: Phase 14 완료, Phase 13 청킹 부분 완료

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

124 lines
4.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)
)
""")
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