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>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
from langchain_core.documents import Document
|
||||
from langchain_qdrant import QdrantVectorStore
|
||||
from qdrant_client import QdrantClient
|
||||
from qdrant_client.models import Filter, FieldCondition, MatchValue, FilterSelector
|
||||
|
||||
|
||||
class RetrieverService:
|
||||
"""Qdrant 벡터 검색 서비스. LangGraph Tool 및 직접 검색 모두 지원."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
embeddings,
|
||||
qdrant_url: str,
|
||||
collection_name: str,
|
||||
top_k: int,
|
||||
):
|
||||
self._client = QdrantClient(url=qdrant_url)
|
||||
self._collection_name = collection_name
|
||||
self._store = QdrantVectorStore(
|
||||
client=self._client,
|
||||
collection_name=collection_name,
|
||||
embedding=embeddings,
|
||||
)
|
||||
self._top_k = top_k
|
||||
|
||||
def as_retriever(self):
|
||||
return self._store.as_retriever(search_kwargs={"k": self._top_k})
|
||||
|
||||
def search(self, query: str) -> list[Document]:
|
||||
return self._store.similarity_search(query, k=self._top_k)
|
||||
|
||||
def list_documents(self) -> list[str]:
|
||||
"""Qdrant에 저장된 고유 파일 경로 목록을 반환한다."""
|
||||
sources: set[str] = set()
|
||||
offset = None
|
||||
while True:
|
||||
results, next_offset = self._client.scroll(
|
||||
collection_name=self._collection_name,
|
||||
with_payload=True,
|
||||
limit=200,
|
||||
offset=offset,
|
||||
)
|
||||
for point in results:
|
||||
src = (point.payload or {}).get("metadata", {}).get("source", "")
|
||||
if src:
|
||||
sources.add(src)
|
||||
if next_offset is None:
|
||||
break
|
||||
offset = next_offset
|
||||
return sorted(sources)
|
||||
|
||||
def delete_document(self, source: str) -> None:
|
||||
"""파일 경로로 저장된 모든 청크를 Qdrant에서 삭제한다."""
|
||||
try:
|
||||
self._client.delete(
|
||||
collection_name=self._collection_name,
|
||||
points_selector=FilterSelector(
|
||||
filter=Filter(
|
||||
must=[FieldCondition(
|
||||
key="metadata.source",
|
||||
match=MatchValue(value=source),
|
||||
)]
|
||||
)
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user