Skip to content

Commit 622f3ae

Browse files
committed
Add README with setup and run instructions for LangGraph + Ollama
- Added steps for virtual environment creation and dependency installation - Included Ollama installation and server startup guide - Documented model pulls (gemma, mistral, llama3, nomic-embed-text) - Provided LangGraph run instructions and browser access info - Added troubleshooting notes for common errors"
1 parent 9368291 commit 622f3ae

File tree

3 files changed

+103
-84
lines changed

3 files changed

+103
-84
lines changed
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
from langgraph.graph import MessagesState
2-
from typing import List, Dict, Any
32

43
class RagState(MessagesState):
5-
"""RAG 그래프 상태 (messages + 검색결과 포함)."""
6-
retrieved_docs: List[Dict[str, Any]] = [] # RAG 검색 결과
7-
context: str = "" # 컨텍스트 텍스트
4+
"""RAG 그래프 상태 (messages 리스트 포함)."""
5+
pass
Lines changed: 99 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,106 @@
11
import os
2-
from langgraph.graph import StateGraph, END
3-
from langchain_community.document_loaders import TextLoader
4-
from langchain.text_splitter import RecursiveCharacterTextSplitter
5-
from langchain_community.embeddings import HuggingFaceInferenceAPIEmbeddings
6-
from langchain_community.vectorstores import Chroma # ✅ Chroma로 교체
7-
from langchain_community.llms import HuggingFaceEndpoint
8-
9-
from .modules.state import RagState
10-
11-
12-
# --------------------------
13-
# 1. 문서 로드 + 분할
14-
# --------------------------
15-
def _load_docs():
16-
if os.path.exists("README.md"):
17-
loader = TextLoader("README.md")
18-
docs = loader.load()
19-
else:
20-
raise FileNotFoundError("README.md를 찾지 못했습니다.")
21-
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
22-
return splitter.split_documents(docs)
2+
from pathlib import Path
3+
from typing import List
234

5+
from langgraph.graph import StateGraph, START, END
6+
from langchain_community.document_loaders import TextLoader
7+
from langchain_text_splitters import RecursiveCharacterTextSplitter
8+
from langchain_core.vectorstores import InMemoryVectorStore
9+
from langchain_community.embeddings import OllamaEmbeddings
10+
from langchain_community.chat_models import ChatOllama
11+
12+
# ❗ 절대 import (relative import 오류 방지)
13+
from casts.modules.state import RagState
14+
15+
16+
# ----------------------------
17+
# 0) README 탐색 & 로드
18+
# ----------------------------
19+
def _find_readme() -> Path:
20+
here = Path(__file__).resolve()
21+
candidates = [
22+
here.parents[1] / "README.md", # Rag_Example/README.md
23+
here.parents[2] / "README.md", # 상위 프로젝트 루트/README.md
24+
]
25+
for p in candidates:
26+
if p.exists():
27+
return p
28+
raise FileNotFoundError("README.md를 찾지 못했습니다. Rag_Example/README.md 를 생성하세요.")
29+
30+
def _load_docs() -> List[str]:
31+
path = _find_readme()
32+
loader = TextLoader(str(path), encoding="utf-8")
33+
docs = loader.load()
34+
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=80)
35+
splits = splitter.split_documents(docs)
36+
return [d.page_content for d in splits]
37+
38+
39+
# ----------------------------
40+
# 1) 임베딩/리트리버 (Ollama)
41+
# ----------------------------
42+
# Ollama 임베딩 서버: nomic-embed-text
43+
_embeddings = OllamaEmbeddings(model="nomic-embed-text", base_url=os.getenv("OLLAMA_BASE_URL", "http://localhost:11434"))
2444

2545
_docs = _load_docs()
26-
27-
# --------------------------
28-
# 2. 임베딩 + 벡터DB (Chroma)
29-
# --------------------------
30-
_embeddings = HuggingFaceInferenceAPIEmbeddings(
31-
api_key=os.getenv("HF_API_KEY"), # HuggingFace API 키 (환경변수)
32-
model_name="sentence-transformers/all-MiniLM-L6-v2",
33-
)
34-
35-
vectorstore = Chroma.from_texts([d.page_content for d in _docs], _embeddings)
36-
retriever = vectorstore.as_retriever()
37-
38-
39-
# --------------------------
40-
# 3. LLM (HuggingFace API)
41-
# --------------------------
42-
llm = HuggingFaceEndpoint(
43-
repo_id="HuggingFaceH4/zephyr-7b-beta", # 원하는 모델로 교체 가능
44-
huggingfacehub_api_token=os.getenv("HF_API_KEY"),
45-
temperature=0.7,
46-
max_new_tokens=256,
47-
return_full_text=False,
46+
# InMemory 벡터스토어 → 추가 패키지 설치 불필요
47+
_vectorstore = InMemoryVectorStore.from_texts(_docs, _embeddings)
48+
_retriever = _vectorstore.as_retriever(search_kwargs={"k": 4})
49+
50+
# ----------------------------
51+
# 2) LLM (Ollama Chat)
52+
# ----------------------------
53+
# 예: mistral / llama3 등. (미리 pull 필요)
54+
_llm = ChatOllama(
55+
model=os.getenv("OLLAMA_MODEL", "mistral"),
56+
base_url=os.getenv("OLLAMA_BASE_URL", "http://localhost:11434"),
57+
temperature=0.2
4858
)
4959

5060

51-
# --------------------------
52-
# 4. 노드 정의
53-
# --------------------------
54-
def retrieve_node(state: RagState) -> RagState:
55-
"""사용자 질문 기반으로 문서 검색"""
56-
query = state["messages"][-1]["content"] if state["messages"] else ""
57-
docs = retriever.get_relevant_documents(query)
58-
state["retrieved_docs"] = [d.page_content for d in docs]
59-
return state
60-
61-
62-
def generate_node(state: RagState) -> RagState:
63-
"""검색 결과 + LLM 기반 답변 생성"""
64-
query = state["messages"][-1]["content"]
65-
context = "\n".join(state.get("retrieved_docs", []))
66-
prompt = f"사용자 질문: {query}\n참고 문서:\n{context}\n\n답변:"
67-
answer = llm.invoke(prompt)
68-
state["messages"].append({"role": "assistant", "content": answer})
69-
return state
70-
71-
72-
# --------------------------
73-
# 5. 그래프 정의
74-
# --------------------------
75-
workflow = StateGraph(RagState)
76-
workflow.add_node("retrieve", retrieve_node)
77-
workflow.add_node("generate", generate_node)
78-
79-
workflow.add_edge("retrieve", "generate")
80-
workflow.set_entry_point("retrieve")
81-
workflow.set_finish_point("generate")
82-
83-
rag_workflow = workflow.compile()
61+
# ----------------------------
62+
# 3) 유틸
63+
# ----------------------------
64+
def _last_user_text(state: RagState) -> str:
65+
if not state["messages"]:
66+
return ""
67+
last = state["messages"][-1]
68+
# dict or BaseMessage 모두 안전 처리
69+
return last["content"] if isinstance(last, dict) else getattr(last, "content", str(last))
70+
71+
72+
# ----------------------------
73+
# 4) 노드
74+
# ----------------------------
75+
def retrieve_node(state: RagState):
76+
query = _last_user_text(state)
77+
docs = _retriever.get_relevant_documents(query)
78+
ctx = "\n\n---\n\n".join(d.page_content for d in docs)
79+
# 컨텍스트를 system 메시지로 누적
80+
return {"messages": [{"role": "system", "content": f"CONTEXT:\n{ctx}" if ctx else "CONTEXT: (no docs)"}]}
81+
82+
def generate_node(state: RagState):
83+
# 누적된 system CONTEXT + user 질문을 하나의 프롬프트로 구성
84+
full = "\n\n".join(m["content"] if isinstance(m, dict) else getattr(m, "content", str(m)) for m in state["messages"])
85+
prompt = (
86+
"You are a helpful RAG assistant. Use the CONTEXT to answer the USER question. "
87+
"If the answer is not in the context, say you are not sure.\n\n"
88+
f"{full}\n\nAnswer in Korean:"
89+
)
90+
out = _llm.invoke(prompt)
91+
text = out.content if hasattr(out, "content") else str(out)
92+
return {"messages": [{"role": "assistant", "content": text}]}
93+
94+
95+
# ----------------------------
96+
# 5) 그래프
97+
# ----------------------------
98+
def rag_workflow():
99+
g = StateGraph(RagState)
100+
g.add_node("retrieve", retrieve_node)
101+
g.add_node("generate", generate_node)
102+
103+
g.add_edge(START, "retrieve")
104+
g.add_edge("retrieve", "generate")
105+
g.add_edge("generate", END)
106+
return g.compile()

sample/Rag_example/langgraph.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
{
22
"dependencies": [
3+
"langgraph",
34
"langchain",
45
"langchain-community",
5-
"gpt4all",
6-
"langgraph",
7-
"sentence-transformers",
8-
"torch"
6+
"langchain-text-splitters"
97
],
108
"graphs": {
119
"main": "./casts/workflow.py:rag_workflow"

0 commit comments

Comments
 (0)