Files
youlbot/services/agent/tools.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

97 lines
4.1 KiB
Python

from datetime import date
from langchain_core.tools import tool
@tool
def get_current_date() -> str:
"""오늘 날짜를 반환합니다. 날짜·기간 관련 질문에 사용하세요."""
return date.today().isoformat()
@tool
def web_search(query: str) -> str:
"""최신 뉴스, 금리, 육아 정책 등 실시간 정보가 필요할 때 사용하세요. 저장된 문서에 없는 최신 정보를 검색합니다."""
from duckduckgo_search import DDGS
with DDGS() as ddgs:
results = list(ddgs.text(query, max_results=5))
if not results:
return "검색 결과가 없습니다."
return "\n\n".join(
f"[{r['title']}]\n{r['body']}\n출처: {r['href']}"
for r in results
)
def make_retriever_tool(retriever_service):
"""as_retriever()를 사용하는 단순 검색 Tool (source_buffer 없음)."""
retriever = retriever_service.as_retriever()
@tool
def search_documents(query: str) -> str:
"""등록된 문서(논문, 육아 가이드, 금융 자료 등)에서 관련 정보를 검색합니다.
육아·금융 관련 질문이 오면 자신의 지식으로 답하기 전에 반드시 이 도구를 먼저 호출하세요.
등록된 문서가 없거나 검색 결과가 없을 때만 자신의 학습 지식을 보조적으로 활용합니다."""
docs = retriever.invoke(query)
if not docs:
return "관련 문서를 찾을 수 없습니다."
return "\n\n".join(
f"[문서 {i + 1}]\n{doc.page_content}" for i, doc in enumerate(docs)
)
return search_documents
def make_memory_tools(profile_repo, user_id: str = "default"):
"""사용자 정보 저장/조회 Tool 쌍을 반환한다."""
@tool
def remember_user_info(key: str, value: str) -> str:
"""사용자 정보를 영구 저장합니다. 다음 대화에도 기억해야 할 정보를 저장하세요.
- 아이 나이는 반드시 '생년(출생연도)'으로 저장하세요. 나이는 매년 바뀌지만 생년은 영구적입니다.
예: key='첫째_이름' value='신도율', key='첫째_생년' value='2020'
- 기타 key 예시: 재정_목표, 거주지, 직업, 자녀수"""
profile_repo.remember(key, value, user_id=user_id)
return f"'{key}' 정보를 기억했습니다: {value}"
@tool
def recall_user_info(key: str) -> str:
"""이전 대화에서 저장한 사용자 정보를 조회합니다."""
value = profile_repo.recall(key, user_id=user_id)
return value if value is not None else f"'{key}'에 대한 저장된 정보가 없습니다."
return remember_user_info, recall_user_info
def make_search_tool(retriever_service, source_buffer: list | None = None):
"""RetrieverService를 클로저로 감싼 문서 검색 Tool을 반환합니다.
source_buffer가 주어지면 검색된 문서의 메타데이터(source, page)를 누적 저장합니다.
"""
@tool
def search_documents(query: str) -> str:
"""등록된 문서(논문, 육아 가이드, 금융 자료 등)에서 관련 정보를 검색합니다.
육아·금융 관련 질문이 오면 자신의 지식으로 답하기 전에 반드시 이 도구를 먼저 호출하세요.
등록된 문서가 없거나 검색 결과가 없을 때만 자신의 학습 지식을 보조적으로 활용합니다."""
docs = retriever_service.search(query)
if source_buffer is not None:
for doc in docs:
src = doc.metadata.get("source", "")
page = doc.metadata.get("page", None)
if src:
entry = {"source": src}
if page is not None:
entry["page"] = page + 1 # 0-indexed → 1-indexed
if entry not in source_buffer:
source_buffer.append(entry)
if not docs:
return "관련 문서를 찾을 수 없습니다."
return "\n\n".join(
f"[문서 {i + 1}]\n{doc.page_content}" for i, doc in enumerate(docs)
)
return search_documents