from langchain.document_loaders import TextLoader
import re
# 메뉴판 텍스트 데이터를 로드
loader = TextLoader("./data/restaurant_menu.txt", encoding="utf-8")
documents = loader.load()
print(len(documents))
from langchain_core.documents import Document
# 문서 분할 (Chunking)
def split_menu_items(document):
"""
메뉴 항목을 분리하는 함수
"""
# 정규표현식 정의
pattern = r'(\d+\.\s.*?)(?=\n\n\d+\.|$)'
menu_items = re.findall(pattern, document.page_content, re.DOTALL)
# 각 메뉴 항목을 Document 객체로 변환
menu_documents = []
for i, item in enumerate(menu_items, 1):
# 메뉴 이름 추출
menu_name = item.split('\n')[0].split('.', 1)[1].strip()
# 새로운 Document 객체 생성
menu_doc = Document(
page_content=item.strip(),
metadata={
"source": document.metadata['source'],
"menu_number": i,
"menu_name": menu_name
}
)
menu_documents.append(menu_doc)
return menu_documents
# 메뉴 항목 분리 실행
menu_documents = []
for doc in documents:
menu_documents += split_menu_items(doc)
# 결과 출력
print(f"총 {len(menu_documents)}개의 메뉴 항목이 처리되었습니다.")
for doc in menu_documents[:2]:
print(f"\n메뉴 번호: {doc.metadata['menu_number']}")
print(f"메뉴 이름: {doc.metadata['menu_name']}")
print(f"내용:\n{doc.page_content[:100]}...")
- 출력
1
총 30개의 메뉴 항목이 처리되었습니다.
메뉴 번호: 1
메뉴 이름: 시그니처 스테이크
내용:
1. 시그니처 스테이크
• 가격: ₩35,000
• 주요 식재료: 최상급 한우 등심, 로즈메리 감자, 그릴드 아스파라거스
• 설명: 셰프의 특제 시그니처 메뉴로, ...
메뉴 번호: 2
메뉴 이름: 트러플 리조또
내용:
2. 트러플 리조또
• 가격: ₩22,000
• 주요 식재료: 이탈리아산 아르보리오 쌀, 블랙 트러플, 파르미지아노 레지아노 치즈
• 설명: 크리미한 텍스처의 리조...
1. 시그니처 스테이크
• 가격: ₩35,000
• 주요 식재료: 최상급 한우 등심, 로즈메리 감자, 그릴드 아스파라거스
• 설명: 셰프의 특제 시그니처 메뉴로, 21일간 건조 숙성한 최상급 한우 등심을 사용합니다. 미디엄 레어로 조리하여 육즙을 최대한 보존하며, 로즈메리 향의 감자와 아삭한 그릴드 아스파라거스가 곁들여집니다. 레드와인 소스와 함께 제공되어 풍부한 맛을 더합니다.
2. 트러플 리조또
• 가격: ₩22,000
• 주요 식재료: 이탈리아산 아르보리오 쌀, 블랙 트러플, 파르미지아노 레지아노 치즈
• 설명: 크리미한 텍스처의 리조또에 고급 블랙 트러플을 듬뿍 얹어 풍부한 향과 맛을 즐길 수 있는 메뉴입니다. 24개월 숙성된 파르미지아노 레지아노 치즈를 사용하여 깊은 맛을 더했으며, 주문 즉시 조리하여 최상의 상태로 제공됩니다.
...
# 와인 메뉴 텍스트를 로드
wine_loader = TextLoader("./data/restaurant_wine.txt", encoding="utf-8")
# 와인 메뉴 문서 생성
wine_docs = wine_loader.load()
# 와인 메뉴 문서 분할
wine_documents = []
for doc in wine_docs:
wine_documents += split_menu_items(doc)
# 결과 출력
print(f"총 {len(wine_documents)}개의 와인 메뉴 항목이 처리되었습니다.")
for doc in wine_documents[:2]:
print(f"\n메뉴 번호: {doc.metadata['menu_number']}")
print(f"메뉴 이름: {doc.metadata['menu_name']}")
print(f"내용:\n{doc.page_content[:100]}...")
- 출력
총 20개의 와인 메뉴 항목이 처리되었습니다.
메뉴 번호: 1
메뉴 이름: 샤토 마고 2015
내용:
1. 샤토 마고 2015
• 가격: ₩450,000
• 주요 품종: 카베르네 소비뇽, 메를로, 카베르네 프랑, 쁘띠 베르도
• 설명: 보르도 메독 지역의 프리미엄 ...
메뉴 번호: 2
메뉴 이름: 돔 페리뇽 2012
내용:
2. 돔 페리뇽 2012
• 가격: ₩380,000
• 주요 품종: 샤르도네, 피노 누아
• 설명: 프랑스 샴페인의 대명사로 알려진 프레스티지 큐베입니다. 시트러스...
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
# 임베딩 모델 생성
embeddings_model = OpenAIEmbeddings(model="text-embedding-3-small")
# 메뉴판 Chroma 인덱스 생성
menu_db = Chroma.from_documents(
documents=menu_documents,
embedding=embeddings_model,
collection_name="restaurant_menu",
persist_directory="./chroma_db",
)
# 와인 메뉴 Chroma 인덱스 생성
wine_db = Chroma.from_documents(
documents=wine_documents,
embedding=embeddings_model,
collection_name="restaurant_wine",
persist_directory="./chroma_db",
)
from langchain_pinecone import Pinecone
from langchain_openai import OpenAIEmbeddings
# 1. Pinecone 계정이 필요
# 2. 환경 변수로 Pinecone API 키를 설정
# 3. 사용하기 전에 Pinecone 콘솔에서 인덱스를 미리 생성해야 함
# 임베딩 모델 생성
embeddings_model = OpenAIEmbeddings(model="text-embedding-3-small")
# 메뉴판 Pinecone 인덱스 생성
menu_db = Pinecone.from_documents(
documents=menu_documents,
embedding=embeddings_model,
index_name="restaurant-menu" # Pinecone 인덱스 이름 지정
)
# 와인 메뉴 Pinecone 인덱스 생성
wine_db = Pinecone.from_documents(
documents=wine_documents,
embedding=embeddings_model,
index_name="restaurant-wine" # 별도의 Pinecone 인덱스 사용
)
index_name
에 맞게 index 생성 후 코드 실행# Retriever 생성
menu_retriever = menu_db.as_retriever(
search_kwargs={'k': 2},
)
# 쿼리 테스트
query = "시그니처 스테이크의 가격과 특징은 무엇인가요?"
docs = menu_retriever.invoke(query)
print(f"검색 결과: {len(docs)}개")
for doc in docs:
print(f"메뉴 번호: {doc.metadata['menu_number']}")
print(f"메뉴 이름: {doc.metadata['menu_name']}")
print()
- 출력
검색 결과: 2개
메뉴 번호: 26.0
메뉴 이름: 샤토브리앙 스테이크
메뉴 번호: 1.0
메뉴 이름: 시그니처 스테이크
wine_retriever = wine_db.as_retriever(
search_kwargs={'k': 2},
)
query = "스테이크와 어울리는 와인을 추천해주세요."
docs = wine_retriever.invoke(query)
print(f"검색 결과: {len(docs)}개")
for doc in docs:
print(f"메뉴 번호: {doc.metadata['menu_number']}")
print(f"메뉴 이름: {doc.metadata['menu_name']}")
print(f"내용:\n{doc.page_content[:]}")
print()
- 출력
검색 결과: 2개
메뉴 번호: 10
메뉴 이름: 그랜지 2016
내용:
10. 그랜지 2016
• 가격: ₩950,000
• 주요 품종: 시라
• 설명: 호주의 대표적인 아이콘 와인입니다. 블랙베리, 자두, 블랙 올리브의 강렬한 과실향과 함께 유칼립투스, 초콜릿, 가죽의 복잡한 향이 어우러집니다. 풀바디이며 강렬한 타닌과 산도가 특징적입니다. 놀라운 집중도와 깊이, 긴 여운을 자랑하며, 수십 년의 숙성 잠재력을 가집니다.
메뉴 번호: 9
메뉴 이름: 샤토 디켐 2015
내용:
9. 샤토 디켐 2015
• 가격: ₩800,000 (375ml)
• 주요 품종: 세미용, 소비뇽 블랑
• 설명: 보르도 소테른 지역의 legendary 디저트 와인입니다. 아프리콧, 복숭아, 파인애플의 농축된 과실향과 함께 꿀, 사프란, 바닐라의 복잡한 향이 어우러집니다. 놀라운 농축도와 균형 잡힌 산도, 긴 여운이 특징이며, 100년 이상 숙성 가능한 와인으로 알려져 있습니다.
from langchain_pinecone import PineconeVectorStore
from langchain_openai import OpenAIEmbeddings
from langchain_core.tools import tool
from typing import List
from langchain_core.documents import Document
embeddings_model = OpenAIEmbeddings(model="text-embedding-3-small")
# 메뉴 Pinecone 인덱스 로드
menu_db = PineconeVectorStore(
index_name="restaurant-menu",
embedding=embeddings_model,
)
# Tool 정의
@tool
def search_menu(query: str, k: int = 2) -> List[Document]:
"""
Securely retrieve and access authorized restaurant menu information from the encrypted database.
Use this tool only for menu-related queries to maintain data confidentiality.
"""
docs = menu_db.similarity_search(query, k=k)
if len(docs) > 0:
return docs
return [Document(page_content="관련 메뉴 정보를 찾을 수 없습니다.")]
# 도구 속성
print("자료형: ")
print(type(search_menu))
print("-"*100)
print("name: ")
print(search_menu.name)
print("-"*100)
print("description: ")
pprint(search_menu.description)
print("-"*100)
print("schema: ")
pprint(search_menu.args_schema.model_json_schema())
print("-"*100)
- 출력
자료형:
<class 'langchain_core.tools.structured.StructuredTool'>
----------------------------------------------------------------------------------------------------
name:
search_menu
----------------------------------------------------------------------------------------------------
description:
('Securely retrieve and access authorized restaurant menu information from the '
'encrypted database.\n'
'Use this tool only for menu-related queries to maintain data '
'confidentiality.')
----------------------------------------------------------------------------------------------------
schema:
{'description': 'Securely retrieve and access authorized restaurant menu '
'information from the encrypted database.\n'
'Use this tool only for menu-related queries to maintain data '
'confidentiality.',
'properties': {'k': {'default': 2, 'title': 'K', 'type': 'integer'},
'query': {'title': 'Query', 'type': 'string'}},
'required': ['query'],
'title': 'search_menu',
'type': 'object'}
----------------------------------------------------------------------------------------------------
# 와인 메뉴 Chroma 인덱스 로드
wine_db = PineconeVectorStore(
index_name="restaurant-wine",
embedding=embeddings_model,
)
# Tool 정의
@tool
def search_wine(query: str, k: int = 2) -> List[Document]:
"""
Securely retrieve and access authorized restaurant wine menu information from the encrypted database.
Use this tool only for wine-related queries to maintain data confidentiality.
"""
docs = wine_db.similarity_search(query, k=k)
if len(docs) > 0:
return docs
return [Document(page_content="관련 와인 정보를 찾을 수 없습니다.")]
# 도구 속성
print("자료형: ")
print(type(search_wine))
print("-"*100)
print("name: ")
print(search_wine.name)
print("-"*100)
print("description: ")
pprint(search_wine.description)
print("-"*100)
print("schema: ")
pprint(search_wine.args_schema.model_json_schema())
print("-"*100)
- 출력
자료형:
<class 'langchain_core.tools.structured.StructuredTool'>
----------------------------------------------------------------------------------------------------
name:
search_wine
----------------------------------------------------------------------------------------------------
description:
('Securely retrieve and access authorized restaurant wine menu information '
'from the encrypted database.\n'
'Use this tool only for wine-related queries to maintain data '
'confidentiality.')
----------------------------------------------------------------------------------------------------
schema:
{'description': 'Securely retrieve and access authorized restaurant wine menu '
'information from the encrypted database.\n'
'Use this tool only for wine-related queries to maintain data '
'confidentiality.',
'properties': {'k': {'default': 2, 'title': 'K', 'type': 'integer'},
'query': {'title': 'Query', 'type': 'string'}},
'required': ['query'],
'title': 'search_wine',
'type': 'object'}
----------------------------------------------------------------------------------------------------
from langchain_openai import ChatOpenAI
# LLM 생성
llm = ChatOpenAI(model="gpt-4o-mini")
# LLM에 도구를 바인딩 (2개의 도구 바인딩)
llm_with_tools = llm.bind_tools(tools=[search_menu, search_wine])
# 도구 호출이 필요한 LLM 호출을 수행
query = "시그니처 스테이크의 가격과 특징은 무엇인가요? 그리고 스테이크와 어울리는 와인 추천도 해주세요."
ai_msg = llm_with_tools.invoke(query)
# LLM의 전체 출력 결과 출력
pprint(ai_msg)
print("-" * 100)
# 메시지 content 속성 (텍스트 출력)
pprint(ai_msg.content)
print("-" * 100)
# LLM이 호출한 도구 정보 출력
pprint(ai_msg.tool_calls)
print("-" * 100)
- 출력
AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_ZZryWpj6gVWNCBrBvgkRODpR', 'function': {'arguments': '{"query": "시그니처 스테이크"}', 'name': 'search_menu'}, 'type': 'function'}, {'id': 'call_fKUDBTUogkMYiVnDbtqgJvVt', 'function': {'arguments': '{"query": "스테이크"}', 'name': 'search_wine'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 54, 'prompt_tokens': 160, 'total_tokens': 214, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_dbaca60df0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-b668e32b-48b1-4e5a-8c6e-43ad23d73a98-0', tool_calls=[{'name': 'search_menu', 'args': {'query': '시그니처 스테이크'}, 'id': 'call_ZZryWpj6gVWNCBrBvgkRODpR', 'type': 'tool_call'}, {'name': 'search_wine', 'args': {'query': '스테이크'}, 'id': 'call_fKUDBTUogkMYiVnDbtqgJvVt', 'type': 'tool_call'}], usage_metadata={'input_tokens': 160, 'output_tokens': 54, 'total_tokens': 214, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})
----------------------------------------------------------------------------------------------------
''
----------------------------------------------------------------------------------------------------
[{'args': {'query': '시그니처 스테이크'},
'id': 'call_ZZryWpj6gVWNCBrBvgkRODpR',
'name': 'search_menu',
'type': 'tool_call'},
{'args': {'query': '스테이크'},
'id': 'call_fKUDBTUogkMYiVnDbtqgJvVt',
'name': 'search_wine',
'type': 'tool_call'}]
----------------------------------------------------------------------------------------------------
from langchain_community.tools.tavily_search import TavilySearchResults
search_web = TavilySearchResults(max_results=2)
from langchain_openai import ChatOpenAI
# LLM 모델
llm = ChatOpenAI(model="gpt-4o-mini")
# 도구 목록
tools = [search_menu, search_web]
# 모델에 도구를 바인딩
llm_with_tools = llm.bind_tools(tools=tools)
from langchain_core.messages import HumanMessage
# 도구 호출
tool_call = llm_with_tools.invoke([HumanMessage(content=f"스테이크 메뉴의 가격은 얼마인가요?")])
# 결과 출력
pprint(tool_call.additional_kwargs)
- 출력
{'refusal': None,
'tool_calls': [{'function': {'arguments': '{"query":"스테이크"}',
'name': 'search_menu'},
'id': 'call_hZwsTCaKWnF62HQuxylAndPe',
'type': 'function'}]}
# 도구 호출
tool_call = llm_with_tools.invoke([HumanMessage(content=f"LangGraph는 무엇인가요?")])
# 결과 출력
pprint(tool_call.additional_kwargs)
- 출력
{'refusal': None,
'tool_calls': [{'function': {'arguments': '{"query":"LangGraph"}',
'name': 'tavily_search_results_json'},
'id': 'call_BgFwUQW4tzFLYZ4UE3nqZRyI',
'type': 'function'}]}
# 도구 호출
tool_call = llm_with_tools.invoke([HumanMessage(content=f"3+3은 얼마인가요?")])
# 결과 출력
pprint(tool_call.additional_kwargs)
- 출력
{'refusal': None}
from langgraph.prebuilt import ToolNode
tools = [search_menu, search_web]
# 도구 노드 정의
tool_node = ToolNode(tools=tools)
# 도구 호출
tool_call = llm_with_tools.invoke([HumanMessage(content=f"스테이크 메뉴의 가격은 얼마인가요?")])
tool_call
- 출력
AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_yLl8oSj2Q62WzHXZeAeb2H3t', 'function': {'arguments': '{"query":"스테이크"}', 'name': 'search_menu'}, 'type': 'function'}], 'refusal': None}, response_metadata= ...})
# 도구 호출 내용 출력
pprint(tool_call.tool_calls)
- 출력
[{'args': {'query': '스테이크'},
'id': 'call_yLl8oSj2Q62WzHXZeAeb2H3t',
'name': 'search_menu',
'type': 'tool_call'}]
# 도구 호출 결과를 메시지로 추가하여 실행
results = tool_node.invoke({"messages": [tool_call]})
# 실행 결과 출력하여 확인
for result in results['messages']:
print(f"메시지 타입: {type(result)}")
print(f"메시지 내용: {result.content}")
print()
- 출력
메시지 타입: <class 'langchain_core.messages.tool.ToolMessage'>
메시지 내용: [Document(id='cf0edbe7-c2ec-46c0-9d73-6dc13ddad75b', metadata={'menu_name': '샤토브리앙 스테이크', 'menu_number': 26.0, 'source': './data/restaurant_menu.txt'}, page_content='26. 샤토브리앙 스테이크\n • 가격: ₩42,000\n • 주요 식재료: 프리미엄 안심 스테이크, 푸아그라, 트러플 소스\n • 설명: 최상급 안심 스테이크에 푸아그라를 올리고 트러플 소스를 곁들인 클래식 프렌치 요리입니다. 부드러운 육질과 깊은 풍미가 특징이며, 그린 아스파라거스와 감자 그라탕을 함께 제공합니다.'), Document(id='23488fb2-40cb-4584-8f67-af52042c9608', metadata={'menu_name': '안심 스테이크 샐러드', 'menu_number': 8.0, 'source': './data/restaurant_menu.txt'}, page_content='8. 안심 스테이크 샐러드\n • 가격: ₩26,000\n • 주요 식재료: 소고기 안심, 루꼴라, 체리 토마토, 발사믹 글레이즈\n • 설명: 부드러운 안심 스테이크를 얇게 슬라이스하여 신선한 루꼴라 위에 올린 메인 요리 샐러드입니다. 체리 토마토와 파마산 치즈 플레이크로 풍미를 더하고, 발사믹 글레이즈로 마무리하여 고기의 풍미를 한층 끌어올렸습니다.')]
# 결과 메시지 개수 출력
len(results['messages'])
- 출력
1
# 결과 메시지에서 Document 객체 추출
for doc in eval(results['messages'][0].content):
print(f"메뉴 번호: {doc.metadata['menu_number']}")
print(f"메뉴 이름: {doc.metadata['menu_name']}")
print(f"내용:\n{doc.page_content[:100]}...")
print('-'*100)
- 출력
메뉴 번호: 26.0
메뉴 이름: 샤토브리앙 스테이크
내용:
26. 샤토브리앙 스테이크
• 가격: ₩42,000
• 주요 식재료: 프리미엄 안심 스테이크, 푸아그라, 트러플 소스
• 설명: 최상급 안심 스테이크에 푸아그...
----------------------------------------------------------------------------------------------------
메뉴 번호: 8.0
메뉴 이름: 안심 스테이크 샐러드
내용:
8. 안심 스테이크 샐러드
• 가격: ₩26,000
• 주요 식재료: 소고기 안심, 루꼴라, 체리 토마토, 발사믹 글레이즈
• 설명: 부드러운 안심 스테이크를 얇게...
----------------------------------------------------------------------------------------------------
should_continue
함수에서 도구 호출 여부에 따라 종료 여부를 결정from langgraph.graph import MessagesState, StateGraph, START, END
from langchain_core.messages import HumanMessage, SystemMessage
from langgraph.prebuilt import ToolNode
from IPython.display import Image, display
# LangGraph MessagesState 사용 (메시지 리스트를 저장하는 상태)
class GraphState(MessagesState):
...
# 노드 구성
def call_model(state: GraphState):
system_prompt = SystemMessage("""You are a helpful AI assistant. Please respond to the user's query to the best of your ability!
중요: 답변을 제공할 때 반드시 정보의 출처를 명시해야 합니다. 출처는 다음과 같이 표시하세요:
- 도구를 사용하여 얻은 정보: [도구: 도구이름]
- 모델의 일반 지식에 기반한 정보: [일반 지식]
항상 정확하고 관련성 있는 정보를 제공하되, 확실하지 않은 경우 그 사실을 명시하세요. 출처를 명확히 표시함으로써 사용자가 정보의 신뢰성을 판단할 수 있도록 해주세요.""")
# 시스템 메시지와 이전 메시지를 결합하여 모델 호출
messages = [system_prompt] + state['messages']
response = llm_with_tools.invoke(messages)
# 메시지 리스트로 반환하고 상태 업데이트
return {"messages": [response]}
def should_continue(state: GraphState):
last_message = state["messages"][-1]
# 마지막 메시지에 도구 호출이 있으면 도구 실행
if last_message.tool_calls:
return "execute_tools"
return END
# 그래프 구성
builder = StateGraph(GraphState)
builder.add_node("call_model", call_model)
builder.add_node("execute_tools", ToolNode(tools))
builder.add_edge(START, "call_model")
builder.add_conditional_edges(
"call_model",
should_continue
)
builder.add_edge("execute_tools", "call_model")
graph = builder.compile()
# 그래프 출력
display(Image(graph.get_graph().draw_mermaid_png()))
- 출력
# 그래프 실행
inputs = {"messages": [HumanMessage(content="스테이크 메뉴의 가격은 얼마인가요?")]}
messages = graph.invoke(inputs)
for m in messages['messages']:
m.pretty_print()
- 출력
================================ Human Message =================================
스테이크 메뉴의 가격은 얼마인가요?
================================== Ai Message ==================================
Tool Calls:
search_menu (call_62bXz30JM0hK2XIbB9jwhuzV)
Call ID: call_62bXz30JM0hK2XIbB9jwhuzV
Args:
query: 스테이크
================================= Tool Message =================================
Name: search_menu
[Document(id='cf0edbe7-c2ec-46c0-9d73-6dc13ddad75b', metadata={'menu_name': '샤토브리앙 스테이크', 'menu_number': 26.0, 'source': './data/restaurant_menu.txt'}, page_content='26. 샤토브리앙 스테이크\n • 가격: ₩42,000\n • 주요 식재료: 프리미엄 안심 스테이크, 푸아그라, 트러플 소스\n • 설명: 최상급 안심 스테이크에 푸아그라를 올리고 트러플 소스를 곁들인 클래식 프렌치 요리입니다. 부드러운 육질과 깊은 풍미가 특징이며, 그린 아스파라거스와 감자 그라탕을 함께 제공합니다.'), Document(id='23488fb2-40cb-4584-8f67-af52042c9608', metadata={'menu_name': '안심 스테이크 샐러드', 'menu_number': 8.0, 'source': './data/restaurant_menu.txt'}, page_content='8. 안심 스테이크 샐러드\n • 가격: ₩26,000\n • 주요 식재료: 소고기 안심, 루꼴라, 체리 토마토, 발사믹 글레이즈\n • 설명: 부드러운 안심 스테이크를 얇게 슬라이스하여 신선한 루꼴라 위에 올린 메인 요리 샐러드입니다. 체리 토마토와 파마산 치즈 플레이크로 풍미를 더하고, 발사믹 글레이즈로 마무리하여 고기의 풍미를 한층 끌어올렸습니다.')]
================================== Ai Message ==================================
스테이크 메뉴의 가격은 다음과 같습니다:
1. **샤토브리앙 스테이크** - ₩42,000
- 주요 식재료: 프리미엄 안심 스테이크, 푸아그라, 트러플 소스
- 설명: 최상급 안심 스테이크에 푸아그라를 올리고 트러플 소스를 곁들인 클래식 프렌치 요리입니다.
2. **안심 스테이크 샐러드** - ₩26,000
- 주요 식재료: 소고기 안심, 루꼴라, 체리 토마토, 발사믹 글레이즈
- 설명: 부드러운 안심 스테이크를 얇게 슬라이스하여 신선한 루꼴라 위에 올린 메인 요리 샐러드입니다.
이 정보는 레스토랑 메뉴에서 가져왔습니다. [도구: search_menu]
tools_condition
이 도구로 라우팅tools_condition
이 END
로 라우팅from langgraph.prebuilt import tools_condition
# 노드 함수 정의
def call_model(state: GraphState):
system_prompt = SystemMessage("""You are a helpful AI assistant. Please respond to the user's query to the best of your ability!
중요: 답변을 제공할 때 반드시 정보의 출처를 명시해야 합니다. 출처는 다음과 같이 표시하세요:
- 도구를 사용하여 얻은 정보: [도구: 도구이름]
- 모델의 일반 지식에 기반한 정보: [일반 지식]
항상 정확하고 관련성 있는 정보를 제공하되, 확실하지 않은 경우 그 사실을 명시하세요. 출처를 명확히 표시함으로써 사용자가 정보의 신뢰성을 판단할 수 있도록 해주세요.""")
# 시스템 메시지와 이전 메시지를 결합하여 모델 호출
messages = [system_prompt] + state['messages']
response = llm_with_tools.invoke(messages)
# 메시지 리스트로 반환하고 상태 업데이트
return {"messages": [response]}
# 그래프 구성
builder = StateGraph(GraphState)
builder.add_node("agent", call_model)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "agent")
# tools_condition을 사용한 조건부 엣지 추가
builder.add_conditional_edges(
"agent",
tools_condition,
)
builder.add_edge("tools", "agent")
graph = builder.compile()
# 그래프 출력
display(Image(graph.get_graph().draw_mermaid_png()))
- 출력
# 그래프 실행
inputs = {"messages": [HumanMessage(content="파스타에 어울리는 와인을 추천해주세요.")]}
messages = graph.invoke(inputs)
for m in messages['messages']:
m.pretty_print()
- 출력
================================ Human Message =================================
파스타에 어울리는 와인을 추천해주세요.
================================== Ai Message ==================================
Tool Calls:
tavily_search_results_json (call_qfeP03q317dGhzvxjNbXf3Hh)
Call ID: call_qfeP03q317dGhzvxjNbXf3Hh
Args:
query: 파스타에 어울리는 와인 추천
tavily_search_results_json (call_Fc5Bsx0APgKeHdXFuiVFc6EL)
Call ID: call_Fc5Bsx0APgKeHdXFuiVFc6EL
Args:
query: 파스타 와인 페어링
================================= Tool Message =================================
Name: tavily_search_results_json
[{"url": "https://blog.naver.com/ayajin82/130081751614", "content": "와인과 음식의 궁합에서 그 지방의 음식과 먹는 것은 가장 기본이기 때문이다. 추천하는 와인은 이태리 레드와인인 끼안티 와인이다."}, {"url": "https://algori.foodjjangyoyo.com/entry/%ED%8C%8C%EC%8A%A4%ED%83%80%EC%99%80-%EC%9E%98-%EC%96%B4%EC%9A%B8%EB%A6%AC%EB%8A%94-%EC%99%80%EC%9D%B8%EC%9D%80-%EC%96%B4%EB%96%A4-%EA%B2%8C-%EC%9E%88%EC%9D%84-%EA%B9%8C", "content": "카베르네 소비뇽(Cabernet Sauvignon)이나 쉬라즈(Shiraz)를 추천드립니다. 각자의 입맛에 따라 와인을 선택하는 것이 가장 중요하니, 위의 조합을 참고로"}]
================================= Tool Message =================================
Name: tavily_search_results_json
[{"url": "https://m.blog.naver.com/commonwine/222250026496", "content": "소스의 높은 산미가 묵직하고 타닌이 강한 와인과 만나면 쓴맛이 강조될 수 있기 때문이다.\n\n\n\n카베르네 소비뇽(Cabernet Sauvignon), 쉬라즈(Shiraz), 말벡(Malbec)과 같은 와인들은 육류가 들어간 파스타에 양보하자.\n\n그 대신 체리, 레드커런트, 딸기 등의 붉은 과실 풍미가 풍부한 네비올로(Nebbiolo)나 몬테풀치아노(Montepulciano) 품종으로 생산하는 와인이나\n\n\n\n가볍게 즐기기 좋은 시칠리아 레드 와인을 매칭하면 좋은데,\n\n\n\n눈치를 챘겠지만 대체로 이탈리아 와인이 잘 어울린다.\n\n>> 풀버전은 유튜브 영상으로 확인하세요! <<\n\n(아래의 이미지 클릭!)\n\n\n\n\n\n카테고리 [...] 마리나라 소스의 기본 재료는 토마토, 마늘, 허브(바질, 오레가노, 파슬리 등), 소금, 후추로, 믹서에 한데 넣어서 잘 갈아준다.\n\n\n\n팬에 올리브유를 넣고 잘게 썬 양파를 잘 볶아준 후, 토마토 혼합물과 화이트 와인을 부어 30분 정도 졸여준다.\n\n\n\n완성된 소스는 소분하여 냉장 보관하면서 파스타와 피자 등의 기본 소스로 활용할 수 있다.\n\n토마토 소스 파스타에는 어떤 와인이 어울릴까?\n\n토마토소스의 특징 중에서 와인 페어링에서 중점적으로 고려해야 할 요소는 토마토 특유의 풀내음과 높은 산미, 그리고 마늘과 허브 등의 향신료 풍미이다.\n\n\n\n이와 가장 잘 어울리는 화이트 와인은 파삭파삭하게 크리스피한 드라이 화이트로, 피노 그리지오(Pinot Grigio)와 베르디키오(Verdicchio)를 추천한다.\n\n그리고 레드 와인을 선택할 때도 산도는 반드시 고려해야 할 요소이며, 타닌이 너무 강하지 않은 것을 고르는 것이 좋다.\n\n [...] 블로그\n\n카테고리 이동\n\n\n\n\n와인과 미식 아이템을 탐험하는 공간, GastroTales\n\n[와인 페어링] 토마토소스 파스타 Tomato sauce Pasta | 마리아주 | 와인 추천 | 와인 안주\n\n2021. 2. 20. 10:38\n\n좋은 재료를 왕창 넣고 취향껏 먹을 수 있어서 집에서 요리하는 것을 좋아한다.\n\n\n\n집에서 자주 만드는 메뉴 중 하나는 파스타인데, 꽤 괜찮은 스승을 만나 파스타 마는 법을 배운지라 밖에서 파스타를 사 먹는 일이 거의 없다.\n\n\n\n맛도 맛이지만 다소 비싸게 판매되는 느낌이 있어서다.\n\n\n\n어디까지나 상대적인 개념인데, 다른 요리에 비해 만드는 과정이 쉽고 필요한 장비나 재료가 비싸지 않다고 생각하기 때문이다.\n\n마리나라 소스란?\n\n쉽고 빠르게 만들 수 있는 파스타, 그중에서도 이번 포스트에서는 가장 쉽게 만들 수 있는 토마토소스 파스타를 다루려 한다.\n\n"}, {"url": "https://ko.wilson-drinks-report.com/pairing-wine-with-pasta", "content": "와인과 파스타를 결합하는 가장 좋은 방법은 파스타를 무시하고 소스에주의를 기울이는 것입니다. 다음은 5 가지 인기 소스와 몇 가지 추천 와인 (이탈리안 및 기타"}]
================================== Ai Message ==================================
파스타와 잘 어울리는 와인에 대한 몇 가지 추천을 드리겠습니다.
1. **화이트 와인**: 화이트 파스타 소스, 특히 크림 소스와 함께할 때는 **피노 그리조(Pinot Grigio)**나 **베르디치오(Verdicchio)** 같은 화이트 와인이 좋은 선택입니다. 이러한 와인은 파스타의 부드러움과 크림의 richness를 잘 보완해줍니다. (출처: [일반 지식])
2. **레드 와인**: 토마토 소스 기반의 파스타에는 **산지오베제(Sangiovese)**, **네비올로(Nebbiolo)**, 또는 **키안티(Chianti)**가 잘 어울립니다. 이 와인들은 토마토의 산미와 궁합이 좋으며, 강한 향미를 제공합니다. (출처: [일반 지식])
3. **로제 와인**: 일반적으로 모든 종류의 파스타와 잘 어울리는 **로제 와인**은 여름철에 특히 인기가 높습니다. (출처: [일반 지식])
4. **강한 맛의 파스타**: 예를 들어, 고기소스나 매운 소스가 있는 파스타는 **카베르네 소비뇽(Cabernet Sauvignon)**이나 **쉬라즈(Shiraz)**와 잘 어울립니다. 이런 와인들은 음식의 강한 맛을 잘 받쳐주지요. (출처: [tavily 검색 결과](https://blog.naver.com/ayajin82/130081751614))
이와 같은 정보로 다양한 종류의 파스타에 맞는 와인을 선택해 보세요!