streamlit-macp-agents / app_KOR.py
jackkuo's picture
fix: track PNGs with LFS
aa98b19
import streamlit as st
import asyncio
import nest_asyncio
import json
import os
import platform
if platform.system() == "Windows":
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
# nest_asyncio 적용: 이미 μ‹€ν–‰ 쀑인 이벀트 루프 λ‚΄μ—μ„œ 쀑첩 호좜 ν—ˆμš©
nest_asyncio.apply()
# μ „μ—­ 이벀트 루프 생성 및 μž¬μ‚¬μš© (ν•œλ²ˆ μƒμ„±ν•œ ν›„ 계속 μ‚¬μš©)
if "event_loop" not in st.session_state:
loop = asyncio.new_event_loop()
st.session_state.event_loop = loop
asyncio.set_event_loop(loop)
from langgraph.prebuilt import create_react_agent
from langchain_anthropic import ChatAnthropic
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from dotenv import load_dotenv
from langchain_mcp_adapters.client import MultiServerMCPClient
from utils import astream_graph, random_uuid
from langchain_core.messages.ai import AIMessageChunk
from langchain_core.messages.tool import ToolMessage
from langgraph.checkpoint.memory import MemorySaver
from langchain_core.runnables import RunnableConfig
# ν™˜κ²½ λ³€μˆ˜ λ‘œλ“œ (.env νŒŒμΌμ—μ„œ API ν‚€ λ“±μ˜ 섀정을 κ°€μ Έμ˜΄)
load_dotenv(override=True)
# config.json 파일 경둜 μ„€μ •
CONFIG_FILE_PATH = "config.json"
# JSON μ„€μ • 파일 λ‘œλ“œ ν•¨μˆ˜
def load_config_from_json():
"""
config.json νŒŒμΌμ—μ„œ 섀정을 λ‘œλ“œν•©λ‹ˆλ‹€.
파일이 μ—†λŠ” 경우 κΈ°λ³Έ μ„€μ •μœΌλ‘œ νŒŒμΌμ„ μƒμ„±ν•©λ‹ˆλ‹€.
λ°˜ν™˜κ°’:
dict: λ‘œλ“œλœ μ„€μ •
"""
default_config = {
"get_current_time": {
"command": "python",
"args": ["./mcp_server_time.py"],
"transport": "stdio"
}
}
try:
if os.path.exists(CONFIG_FILE_PATH):
with open(CONFIG_FILE_PATH, "r", encoding="utf-8") as f:
return json.load(f)
else:
# 파일이 μ—†λŠ” 경우 κΈ°λ³Έ μ„€μ •μœΌλ‘œ 파일 생성
save_config_to_json(default_config)
return default_config
except Exception as e:
st.error(f"μ„€μ • 파일 λ‘œλ“œ 쀑 였λ₯˜ λ°œμƒ: {str(e)}")
return default_config
# JSON μ„€μ • 파일 μ €μž₯ ν•¨μˆ˜
def save_config_to_json(config):
"""
섀정을 config.json νŒŒμΌμ— μ €μž₯ν•©λ‹ˆλ‹€.
λ§€κ°œλ³€μˆ˜:
config (dict): μ €μž₯ν•  μ„€μ •
λ°˜ν™˜κ°’:
bool: μ €μž₯ 성곡 μ—¬λΆ€
"""
try:
with open(CONFIG_FILE_PATH, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
st.error(f"μ„€μ • 파일 μ €μž₯ 쀑 였λ₯˜ λ°œμƒ: {str(e)}")
return False
# 둜그인 μ„Έμ…˜ λ³€μˆ˜ μ΄ˆκΈ°ν™”
if "authenticated" not in st.session_state:
st.session_state.authenticated = False
# 둜그인 ν•„μš” μ—¬λΆ€ 확인
use_login = os.environ.get("USE_LOGIN", "false").lower() == "true"
# 둜그인 μƒνƒœμ— 따라 νŽ˜μ΄μ§€ μ„€μ • λ³€κ²½
if use_login and not st.session_state.authenticated:
# 둜그인 νŽ˜μ΄μ§€λŠ” κΈ°λ³Έ(narrow) λ ˆμ΄μ•„μ›ƒ μ‚¬μš©
st.set_page_config(page_title="Agent with MCP Tools", page_icon="🧠")
else:
# 메인 앱은 wide λ ˆμ΄μ•„μ›ƒ μ‚¬μš©
st.set_page_config(page_title="Agent with MCP Tools", page_icon="🧠", layout="wide")
# 둜그인 κΈ°λŠ₯이 ν™œμ„±ν™”λ˜μ–΄ 있고 아직 μΈμ¦λ˜μ§€ μ•Šμ€ 경우 둜그인 ν™”λ©΄ ν‘œμ‹œ
if use_login and not st.session_state.authenticated:
st.title("πŸ” 둜그인")
st.markdown("μ‹œμŠ€ν…œμ„ μ‚¬μš©ν•˜λ €λ©΄ 둜그인이 ν•„μš”ν•©λ‹ˆλ‹€.")
# 둜그인 폼을 ν™”λ©΄ 쀑앙에 쒁게 배치
with st.form("login_form"):
username = st.text_input("아이디")
password = st.text_input("λΉ„λ°€λ²ˆν˜Έ", type="password")
submit_button = st.form_submit_button("둜그인")
if submit_button:
expected_username = os.environ.get("USER_ID")
expected_password = os.environ.get("USER_PASSWORD")
if username == expected_username and password == expected_password:
st.session_state.authenticated = True
st.success("βœ… 둜그인 성곡! μž μ‹œλ§Œ κΈ°λ‹€λ €μ£Όμ„Έμš”...")
st.rerun()
else:
st.error("❌ 아이디 λ˜λŠ” λΉ„λ°€λ²ˆν˜Έκ°€ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")
# 둜그인 ν™”λ©΄μ—μ„œλŠ” 메인 앱을 ν‘œμ‹œν•˜μ§€ μ•ŠμŒ
st.stop()
# μ‚¬μ΄λ“œλ°” μ΅œμƒλ‹¨μ— μ €μž 정보 μΆ”κ°€ (λ‹€λ₯Έ μ‚¬μ΄λ“œλ°” μš”μ†Œλ³΄λ‹€ λ¨Όμ € 배치)
st.sidebar.markdown("### ✍️ Made by [ν…Œλ””λ…ΈνŠΈ](https://youtube.com/c/teddynote) πŸš€")
st.sidebar.markdown(
"### πŸ’» [Project Page](https://github.com/teddynote-lab/langgraph-mcp-agents)"
)
st.sidebar.divider() # ꡬ뢄선 μΆ”κ°€
# κΈ°μ‘΄ νŽ˜μ΄μ§€ 타이틀 및 μ„€λͺ…
st.title("πŸ’¬ MCP 도ꡬ ν™œμš© μ—μ΄μ „νŠΈ")
st.markdown("✨ MCP 도ꡬλ₯Ό ν™œμš©ν•œ ReAct μ—μ΄μ „νŠΈμ—κ²Œ μ§ˆλ¬Έν•΄λ³΄μ„Έμš”.")
SYSTEM_PROMPT = """<ROLE>
You are a smart agent with an ability to use tools.
You will be given a question and you will use the tools to answer the question.
Pick the most relevant tool to answer the question.
If you are failed to answer the question, try different tools to get context.
Your answer should be very polite and professional.
</ROLE>
----
<INSTRUCTIONS>
Step 1: Analyze the question
- Analyze user's question and final goal.
- If the user's question is consist of multiple sub-questions, split them into smaller sub-questions.
Step 2: Pick the most relevant tool
- Pick the most relevant tool to answer the question.
- If you are failed to answer the question, try different tools to get context.
Step 3: Answer the question
- Answer the question in the same language as the question.
- Your answer should be very polite and professional.
Step 4: Provide the source of the answer(if applicable)
- If you've used the tool, provide the source of the answer.
- Valid sources are either a website(URL) or a document(PDF, etc).
Guidelines:
- If you've used the tool, your answer should be based on the tool's output(tool's output is more important than your own knowledge).
- If you've used the tool, and the source is valid URL, provide the source(URL) of the answer.
- Skip providing the source if the source is not URL.
- Answer in the same language as the question.
- Answer should be concise and to the point.
- Avoid response your output with any other information than the answer and the source.
</INSTRUCTIONS>
----
<OUTPUT_FORMAT>
(concise answer to the question)
**Source**(if applicable)
- (source1: valid URL)
- (source2: valid URL)
- ...
</OUTPUT_FORMAT>
"""
OUTPUT_TOKEN_INFO = {
"claude-3-5-sonnet-latest": {"max_tokens": 8192},
"claude-3-5-haiku-latest": {"max_tokens": 8192},
"claude-3-7-sonnet-latest": {"max_tokens": 64000},
"gpt-4o": {"max_tokens": 16000},
"gpt-4o-mini": {"max_tokens": 16000},
}
# μ„Έμ…˜ μƒνƒœ μ΄ˆκΈ°ν™”
if "session_initialized" not in st.session_state:
st.session_state.session_initialized = False # μ„Έμ…˜ μ΄ˆκΈ°ν™” μƒνƒœ ν”Œλž˜κ·Έ
st.session_state.agent = None # ReAct μ—μ΄μ „νŠΈ 객체 μ €μž₯ 곡간
st.session_state.history = [] # λŒ€ν™” 기둝 μ €μž₯ 리슀트
st.session_state.mcp_client = None # MCP ν΄λΌμ΄μ–ΈνŠΈ 객체 μ €μž₯ 곡간
st.session_state.timeout_seconds = 120 # 응닡 생성 μ œν•œ μ‹œκ°„(초), κΈ°λ³Έκ°’ 120초
st.session_state.selected_model = "claude-3-7-sonnet-latest" # κΈ°λ³Έ λͺ¨λΈ 선택
st.session_state.recursion_limit = 100 # μž¬κ·€ 호좜 μ œν•œ, κΈ°λ³Έκ°’ 100
if "thread_id" not in st.session_state:
st.session_state.thread_id = random_uuid()
# --- ν•¨μˆ˜ μ •μ˜ λΆ€λΆ„ ---
async def cleanup_mcp_client():
"""
κΈ°μ‘΄ MCP ν΄λΌμ΄μ–ΈνŠΈλ₯Ό μ•ˆμ „ν•˜κ²Œ μ’…λ£Œν•©λ‹ˆλ‹€.
κΈ°μ‘΄ ν΄λΌμ΄μ–ΈνŠΈκ°€ μžˆλŠ” 경우 μ •μƒμ μœΌλ‘œ λ¦¬μ†ŒμŠ€λ₯Ό ν•΄μ œν•©λ‹ˆλ‹€.
"""
if "mcp_client" in st.session_state and st.session_state.mcp_client is not None:
try:
await st.session_state.mcp_client.__aexit__(None, None, None)
st.session_state.mcp_client = None
except Exception as e:
import traceback
# st.warning(f"MCP ν΄λΌμ΄μ–ΈνŠΈ μ’…λ£Œ 쀑 였λ₯˜: {str(e)}")
# st.warning(traceback.format_exc())
def print_message():
"""
μ±„νŒ… 기둝을 화면에 좜λ ₯ν•©λ‹ˆλ‹€.
μ‚¬μš©μžμ™€ μ–΄μ‹œμŠ€ν„΄νŠΈμ˜ λ©”μ‹œμ§€λ₯Ό κ΅¬λΆ„ν•˜μ—¬ 화면에 ν‘œμ‹œν•˜κ³ ,
도ꡬ 호좜 μ •λ³΄λŠ” μ–΄μ‹œμŠ€ν„΄νŠΈ λ©”μ‹œμ§€ μ»¨ν…Œμ΄λ„ˆ 내에 ν‘œμ‹œν•©λ‹ˆλ‹€.
"""
i = 0
while i < len(st.session_state.history):
message = st.session_state.history[i]
if message["role"] == "user":
st.chat_message("user", avatar="πŸ§‘β€πŸ’»").markdown(message["content"])
i += 1
elif message["role"] == "assistant":
# μ–΄μ‹œμŠ€ν„΄νŠΈ λ©”μ‹œμ§€ μ»¨ν…Œμ΄λ„ˆ 생성
with st.chat_message("assistant", avatar="πŸ€–"):
# μ–΄μ‹œμŠ€ν„΄νŠΈ λ©”μ‹œμ§€ λ‚΄μš© ν‘œμ‹œ
st.markdown(message["content"])
# λ‹€μŒ λ©”μ‹œμ§€κ°€ 도ꡬ 호좜 정보인지 확인
if (
i + 1 < len(st.session_state.history)
and st.session_state.history[i + 1]["role"] == "assistant_tool"
):
# 도ꡬ 호좜 정보λ₯Ό λ™μΌν•œ μ»¨ν…Œμ΄λ„ˆ 내에 expander둜 ν‘œμ‹œ
with st.expander("πŸ”§ 도ꡬ 호좜 정보", expanded=False):
st.markdown(st.session_state.history[i + 1]["content"])
i += 2 # 두 λ©”μ‹œμ§€λ₯Ό ν•¨κ»˜ μ²˜λ¦¬ν–ˆμœΌλ―€λ‘œ 2 증가
else:
i += 1 # 일반 λ©”μ‹œμ§€λ§Œ μ²˜λ¦¬ν–ˆμœΌλ―€λ‘œ 1 증가
else:
# assistant_tool λ©”μ‹œμ§€λŠ” μœ„μ—μ„œ μ²˜λ¦¬λ˜λ―€λ‘œ κ±΄λ„ˆλœ€
i += 1
def get_streaming_callback(text_placeholder, tool_placeholder):
"""
슀트리밍 콜백 ν•¨μˆ˜λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
이 ν•¨μˆ˜λŠ” LLMμ—μ„œ μƒμ„±λ˜λŠ” 응닡을 μ‹€μ‹œκ°„μœΌλ‘œ 화면에 ν‘œμ‹œν•˜κΈ° μœ„ν•œ 콜백 ν•¨μˆ˜λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
ν…μŠ€νŠΈ 응닡과 도ꡬ 호좜 정보λ₯Ό 각각 λ‹€λ₯Έ μ˜μ—­μ— ν‘œμ‹œν•©λ‹ˆλ‹€.
λ§€κ°œλ³€μˆ˜:
text_placeholder: ν…μŠ€νŠΈ 응닡을 ν‘œμ‹œν•  Streamlit μ»΄ν¬λ„ŒνŠΈ
tool_placeholder: 도ꡬ 호좜 정보λ₯Ό ν‘œμ‹œν•  Streamlit μ»΄ν¬λ„ŒνŠΈ
λ°˜ν™˜κ°’:
callback_func: 슀트리밍 콜백 ν•¨μˆ˜
accumulated_text: λˆ„μ λœ ν…μŠ€νŠΈ 응닡을 μ €μž₯ν•˜λŠ” 리슀트
accumulated_tool: λˆ„μ λœ 도ꡬ 호좜 정보λ₯Ό μ €μž₯ν•˜λŠ” 리슀트
"""
accumulated_text = []
accumulated_tool = []
def callback_func(message: dict):
nonlocal accumulated_text, accumulated_tool
message_content = message.get("content", None)
if isinstance(message_content, AIMessageChunk):
content = message_content.content
# μ½˜ν…μΈ κ°€ 리슀트 ν˜•νƒœμΈ 경우 (Claude λͺ¨λΈ λ“±μ—μ„œ 주둜 λ°œμƒ)
if isinstance(content, list) and len(content) > 0:
message_chunk = content[0]
# ν…μŠ€νŠΈ νƒ€μž…μΈ 경우 처리
if message_chunk["type"] == "text":
accumulated_text.append(message_chunk["text"])
text_placeholder.markdown("".join(accumulated_text))
# 도ꡬ μ‚¬μš© νƒ€μž…μΈ 경우 처리
elif message_chunk["type"] == "tool_use":
if "partial_json" in message_chunk:
accumulated_tool.append(message_chunk["partial_json"])
else:
tool_call_chunks = message_content.tool_call_chunks
tool_call_chunk = tool_call_chunks[0]
accumulated_tool.append(
"\n```json\n" + str(tool_call_chunk) + "\n```\n"
)
with tool_placeholder.expander("πŸ”§ 도ꡬ 호좜 정보", expanded=True):
st.markdown("".join(accumulated_tool))
# tool_calls 속성이 μžˆλŠ” 경우 처리 (OpenAI λͺ¨λΈ λ“±μ—μ„œ 주둜 λ°œμƒ)
elif (
hasattr(message_content, "tool_calls")
and message_content.tool_calls
and len(message_content.tool_calls[0]["name"]) > 0
):
tool_call_info = message_content.tool_calls[0]
accumulated_tool.append("\n```json\n" + str(tool_call_info) + "\n```\n")
with tool_placeholder.expander("πŸ”§ 도ꡬ 호좜 정보", expanded=True):
st.markdown("".join(accumulated_tool))
# λ‹¨μˆœ λ¬Έμžμ—΄μΈ 경우 처리
elif isinstance(content, str):
accumulated_text.append(content)
text_placeholder.markdown("".join(accumulated_text))
# μœ νš¨ν•˜μ§€ μ•Šμ€ 도ꡬ 호좜 정보가 μžˆλŠ” 경우 처리
elif (
hasattr(message_content, "invalid_tool_calls")
and message_content.invalid_tool_calls
):
tool_call_info = message_content.invalid_tool_calls[0]
accumulated_tool.append("\n```json\n" + str(tool_call_info) + "\n```\n")
with tool_placeholder.expander(
"πŸ”§ 도ꡬ 호좜 정보 (μœ νš¨ν•˜μ§€ μ•ŠμŒ)", expanded=True
):
st.markdown("".join(accumulated_tool))
# tool_call_chunks 속성이 μžˆλŠ” 경우 처리
elif (
hasattr(message_content, "tool_call_chunks")
and message_content.tool_call_chunks
):
tool_call_chunk = message_content.tool_call_chunks[0]
accumulated_tool.append(
"\n```json\n" + str(tool_call_chunk) + "\n```\n"
)
with tool_placeholder.expander("πŸ”§ 도ꡬ 호좜 정보", expanded=True):
st.markdown("".join(accumulated_tool))
# additional_kwargs에 tool_callsκ°€ μžˆλŠ” 경우 처리 (λ‹€μ–‘ν•œ λͺ¨λΈ ν˜Έν™˜μ„± 지원)
elif (
hasattr(message_content, "additional_kwargs")
and "tool_calls" in message_content.additional_kwargs
):
tool_call_info = message_content.additional_kwargs["tool_calls"][0]
accumulated_tool.append("\n```json\n" + str(tool_call_info) + "\n```\n")
with tool_placeholder.expander("πŸ”§ 도ꡬ 호좜 정보", expanded=True):
st.markdown("".join(accumulated_tool))
# 도ꡬ λ©”μ‹œμ§€μΈ 경우 처리 (λ„κ΅¬μ˜ 응닡)
elif isinstance(message_content, ToolMessage):
accumulated_tool.append(
"\n```json\n" + str(message_content.content) + "\n```\n"
)
with tool_placeholder.expander("πŸ”§ 도ꡬ 호좜 정보", expanded=True):
st.markdown("".join(accumulated_tool))
return None
return callback_func, accumulated_text, accumulated_tool
async def process_query(query, text_placeholder, tool_placeholder, timeout_seconds=60):
"""
μ‚¬μš©μž μ§ˆλ¬Έμ„ μ²˜λ¦¬ν•˜κ³  응닡을 μƒμ„±ν•©λ‹ˆλ‹€.
이 ν•¨μˆ˜λŠ” μ‚¬μš©μžμ˜ μ§ˆλ¬Έμ„ μ—μ΄μ „νŠΈμ— μ „λ‹¬ν•˜κ³ , 응닡을 μ‹€μ‹œκ°„μœΌλ‘œ μŠ€νŠΈλ¦¬λ°ν•˜μ—¬ ν‘œμ‹œν•©λ‹ˆλ‹€.
μ§€μ •λœ μ‹œκ°„ 내에 응닡이 μ™„λ£Œλ˜μ§€ μ•ŠμœΌλ©΄ νƒ€μž„μ•„μ›ƒ 였λ₯˜λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.
λ§€κ°œλ³€μˆ˜:
query: μ‚¬μš©μžκ°€ μž…λ ₯ν•œ 질문 ν…μŠ€νŠΈ
text_placeholder: ν…μŠ€νŠΈ 응닡을 ν‘œμ‹œν•  Streamlit μ»΄ν¬λ„ŒνŠΈ
tool_placeholder: 도ꡬ 호좜 정보λ₯Ό ν‘œμ‹œν•  Streamlit μ»΄ν¬λ„ŒνŠΈ
timeout_seconds: 응닡 생성 μ œν•œ μ‹œκ°„(초)
λ°˜ν™˜κ°’:
response: μ—μ΄μ „νŠΈμ˜ 응닡 객체
final_text: μ΅œμ’… ν…μŠ€νŠΈ 응닡
final_tool: μ΅œμ’… 도ꡬ 호좜 정보
"""
try:
if st.session_state.agent:
streaming_callback, accumulated_text_obj, accumulated_tool_obj = (
get_streaming_callback(text_placeholder, tool_placeholder)
)
try:
response = await asyncio.wait_for(
astream_graph(
st.session_state.agent,
{"messages": [HumanMessage(content=query)]},
callback=streaming_callback,
config=RunnableConfig(
recursion_limit=st.session_state.recursion_limit,
thread_id=st.session_state.thread_id,
),
),
timeout=timeout_seconds,
)
except asyncio.TimeoutError:
error_msg = f"⏱️ μš”μ²­ μ‹œκ°„μ΄ {timeout_seconds}초λ₯Ό μ΄ˆκ³Όν–ˆμŠ΅λ‹ˆλ‹€. λ‚˜μ€‘μ— λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”."
return {"error": error_msg}, error_msg, ""
final_text = "".join(accumulated_text_obj)
final_tool = "".join(accumulated_tool_obj)
return response, final_text, final_tool
else:
return (
{"error": "🚫 μ—μ΄μ „νŠΈκ°€ μ΄ˆκΈ°ν™”λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."},
"🚫 μ—μ΄μ „νŠΈκ°€ μ΄ˆκΈ°ν™”λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.",
"",
)
except Exception as e:
import traceback
error_msg = f"❌ 쿼리 처리 쀑 였λ₯˜ λ°œμƒ: {str(e)}\n{traceback.format_exc()}"
return {"error": error_msg}, error_msg, ""
async def initialize_session(mcp_config=None):
"""
MCP μ„Έμ…˜κ³Ό μ—μ΄μ „νŠΈλ₯Ό μ΄ˆκΈ°ν™”ν•©λ‹ˆλ‹€.
λ§€κ°œλ³€μˆ˜:
mcp_config: MCP 도ꡬ μ„€μ • 정보(JSON). None인 경우 κΈ°λ³Έ μ„€μ • μ‚¬μš©
λ°˜ν™˜κ°’:
bool: μ΄ˆκΈ°ν™” 성곡 μ—¬λΆ€
"""
with st.spinner("πŸ”„ MCP μ„œλ²„μ— μ—°κ²° 쀑..."):
# λ¨Όμ € κΈ°μ‘΄ ν΄λΌμ΄μ–ΈνŠΈλ₯Ό μ•ˆμ „ν•˜κ²Œ 정리
await cleanup_mcp_client()
if mcp_config is None:
# config.json νŒŒμΌμ—μ„œ μ„€μ • λ‘œλ“œ
mcp_config = load_config_from_json()
client = MultiServerMCPClient(mcp_config)
await client.__aenter__()
tools = client.get_tools()
st.session_state.tool_count = len(tools)
st.session_state.mcp_client = client
# μ„ νƒλœ λͺ¨λΈμ— 따라 μ μ ˆν•œ λͺ¨λΈ μ΄ˆκΈ°ν™”
selected_model = st.session_state.selected_model
if selected_model in [
"claude-3-7-sonnet-latest",
"claude-3-5-sonnet-latest",
"claude-3-5-haiku-latest",
]:
model = ChatAnthropic(
model=selected_model,
temperature=0.1,
max_tokens=OUTPUT_TOKEN_INFO[selected_model]["max_tokens"],
)
else: # OpenAI λͺ¨λΈ μ‚¬μš©
model = ChatOpenAI(
model=selected_model,
temperature=0.1,
max_tokens=OUTPUT_TOKEN_INFO[selected_model]["max_tokens"],
)
agent = create_react_agent(
model,
tools,
checkpointer=MemorySaver(),
prompt=SYSTEM_PROMPT,
)
st.session_state.agent = agent
st.session_state.session_initialized = True
return True
# --- μ‚¬μ΄λ“œλ°”: μ‹œμŠ€ν…œ μ„€μ • μ„Ήμ…˜ ---
with st.sidebar:
st.subheader("βš™οΈ μ‹œμŠ€ν…œ μ„€μ •")
# λͺ¨λΈ 선택 κΈ°λŠ₯
# μ‚¬μš© κ°€λŠ₯ν•œ λͺ¨λΈ λͺ©λ‘ 생성
available_models = []
# Anthropic API ν‚€ 확인
has_anthropic_key = os.environ.get("ANTHROPIC_API_KEY") is not None
if has_anthropic_key:
available_models.extend(
[
"claude-3-7-sonnet-latest",
"claude-3-5-sonnet-latest",
"claude-3-5-haiku-latest",
]
)
# OpenAI API ν‚€ 확인
has_openai_key = os.environ.get("OPENAI_API_KEY") is not None
if has_openai_key:
available_models.extend(["gpt-4o", "gpt-4o-mini"])
# μ‚¬μš© κ°€λŠ₯ν•œ λͺ¨λΈμ΄ μ—†λŠ” 경우 λ©”μ‹œμ§€ ν‘œμ‹œ
if not available_models:
st.warning(
"⚠️ API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. .env νŒŒμΌμ— ANTHROPIC_API_KEY λ˜λŠ” OPENAI_API_KEYλ₯Ό μΆ”κ°€ν•΄μ£Όμ„Έμš”."
)
# κΈ°λ³Έκ°’μœΌλ‘œ Claude λͺ¨λΈ μΆ”κ°€ (ν‚€κ°€ 없어도 UIλ₯Ό 보여주기 μœ„ν•¨)
available_models = ["claude-3-7-sonnet-latest"]
# λͺ¨λΈ 선택 λ“œλ‘­λ‹€μš΄
previous_model = st.session_state.selected_model
st.session_state.selected_model = st.selectbox(
"πŸ€– μ‚¬μš©ν•  λͺ¨λΈ 선택",
options=available_models,
index=(
available_models.index(st.session_state.selected_model)
if st.session_state.selected_model in available_models
else 0
),
help="Anthropic λͺ¨λΈμ€ ANTHROPIC_API_KEYκ°€, OpenAI λͺ¨λΈμ€ OPENAI_API_KEYκ°€ ν™˜κ²½λ³€μˆ˜λ‘œ μ„€μ •λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€.",
)
# λͺ¨λΈμ΄ λ³€κ²½λ˜μ—ˆμ„ λ•Œ μ„Έμ…˜ μ΄ˆκΈ°ν™” ν•„μš” μ•Œλ¦Ό
if (
previous_model != st.session_state.selected_model
and st.session_state.session_initialized
):
st.warning(
"⚠️ λͺ¨λΈμ΄ λ³€κ²½λ˜μ—ˆμŠ΅λ‹ˆλ‹€. 'μ„€μ • μ μš©ν•˜κΈ°' λ²„νŠΌμ„ 눌러 변경사항을 μ μš©ν•˜μ„Έμš”."
)
# νƒ€μž„μ•„μ›ƒ μ„€μ • μŠ¬λΌμ΄λ” μΆ”κ°€
st.session_state.timeout_seconds = st.slider(
"⏱️ 응닡 생성 μ œν•œ μ‹œκ°„(초)",
min_value=60,
max_value=300,
value=st.session_state.timeout_seconds,
step=10,
help="μ—μ΄μ „νŠΈκ°€ 응닡을 μƒμ„±ν•˜λŠ” μ΅œλŒ€ μ‹œκ°„μ„ μ„€μ •ν•©λ‹ˆλ‹€. λ³΅μž‘ν•œ μž‘μ—…μ€ 더 κΈ΄ μ‹œκ°„μ΄ ν•„μš”ν•  수 μžˆμŠ΅λ‹ˆλ‹€.",
)
st.session_state.recursion_limit = st.slider(
"⏱️ μž¬κ·€ 호좜 μ œν•œ(횟수)",
min_value=10,
max_value=200,
value=st.session_state.recursion_limit,
step=10,
help="μž¬κ·€ 호좜 μ œν•œ 횟수λ₯Ό μ„€μ •ν•©λ‹ˆλ‹€. λ„ˆλ¬΄ 높은 값을 μ„€μ •ν•˜λ©΄ λ©”λͺ¨λ¦¬ λΆ€μ‘± λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.",
)
st.divider() # ꡬ뢄선 μΆ”κ°€
# 도ꡬ μ„€μ • μ„Ήμ…˜ μΆ”κ°€
st.subheader("πŸ”§ 도ꡬ μ„€μ •")
# expander μƒνƒœλ₯Ό μ„Έμ…˜ μƒνƒœλ‘œ 관리
if "mcp_tools_expander" not in st.session_state:
st.session_state.mcp_tools_expander = False
# MCP 도ꡬ μΆ”κ°€ μΈν„°νŽ˜μ΄μŠ€
with st.expander("🧰 MCP 도ꡬ μΆ”κ°€", expanded=st.session_state.mcp_tools_expander):
# config.json νŒŒμΌμ—μ„œ μ„€μ • λ‘œλ“œν•˜μ—¬ ν‘œμ‹œ
loaded_config = load_config_from_json()
default_config_text = json.dumps(loaded_config, indent=2, ensure_ascii=False)
# pending configκ°€ μ—†μœΌλ©΄ κΈ°μ‘΄ mcp_config_text 기반으둜 생성
if "pending_mcp_config" not in st.session_state:
try:
st.session_state.pending_mcp_config = loaded_config
except Exception as e:
st.error(f"초기 pending config μ„€μ • μ‹€νŒ¨: {e}")
# κ°œλ³„ 도ꡬ μΆ”κ°€λ₯Ό μœ„ν•œ UI
st.subheader("도ꡬ μΆ”κ°€")
st.markdown(
"""
[μ–΄λ–»κ²Œ μ„€μ • ν•˜λ‚˜μš”?](https://teddylee777.notion.site/MCP-1d324f35d12980c8b018e12afdf545a1?pvs=4)
⚠️ **μ€‘μš”**: JSON을 λ°˜λ“œμ‹œ μ€‘κ΄„ν˜Έ(`{}`)둜 감싸야 ν•©λ‹ˆλ‹€."""
)
# 보닀 λͺ…ν™•ν•œ μ˜ˆμ‹œ 제곡
example_json = {
"github": {
"command": "npx",
"args": [
"-y",
"@smithery/cli@latest",
"run",
"@smithery-ai/github",
"--config",
'{"githubPersonalAccessToken":"your_token_here"}',
],
"transport": "stdio",
}
}
default_text = json.dumps(example_json, indent=2, ensure_ascii=False)
new_tool_json = st.text_area(
"도ꡬ JSON",
default_text,
height=250,
)
# μΆ”κ°€ν•˜κΈ° λ²„νŠΌ
if st.button(
"도ꡬ μΆ”κ°€",
type="primary",
key="add_tool_button",
use_container_width=True,
):
try:
# μž…λ ₯κ°’ 검증
if not new_tool_json.strip().startswith(
"{"
) or not new_tool_json.strip().endswith("}"):
st.error("JSON은 μ€‘κ΄„ν˜Έ({})둜 μ‹œμž‘ν•˜κ³  λλ‚˜μ•Ό ν•©λ‹ˆλ‹€.")
st.markdown('μ˜¬λ°”λ₯Έ ν˜•μ‹: `{ "도ꡬ이름": { ... } }`')
else:
# JSON νŒŒμ‹±
parsed_tool = json.loads(new_tool_json)
# mcpServers ν˜•μ‹μΈμ§€ ν™•μΈν•˜κ³  처리
if "mcpServers" in parsed_tool:
# mcpServers μ•ˆμ˜ λ‚΄μš©μ„ μ΅œμƒμœ„λ‘œ 이동
parsed_tool = parsed_tool["mcpServers"]
st.info(
"'mcpServers' ν˜•μ‹μ΄ κ°μ§€λ˜μ—ˆμŠ΅λ‹ˆλ‹€. μžλ™μœΌλ‘œ λ³€ν™˜ν•©λ‹ˆλ‹€."
)
# μž…λ ₯된 도ꡬ 수 확인
if len(parsed_tool) == 0:
st.error("μ΅œμ†Œ ν•˜λ‚˜ μ΄μƒμ˜ 도ꡬλ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.")
else:
# λͺ¨λ“  도ꡬ에 λŒ€ν•΄ 처리
success_tools = []
for tool_name, tool_config in parsed_tool.items():
# URL ν•„λ“œ 확인 및 transport μ„€μ •
if "url" in tool_config:
# URL이 μžˆλŠ” 경우 transportλ₯Ό "sse"둜 μ„€μ •
tool_config["transport"] = "sse"
st.info(
f"'{tool_name}' 도ꡬ에 URL이 κ°μ§€λ˜μ–΄ transportλ₯Ό 'sse'둜 μ„€μ •ν–ˆμŠ΅λ‹ˆλ‹€."
)
elif "transport" not in tool_config:
# URL이 μ—†κ³  transport도 μ—†λŠ” 경우 κΈ°λ³Έκ°’ "stdio" μ„€μ •
tool_config["transport"] = "stdio"
# ν•„μˆ˜ ν•„λ“œ 확인
if (
"command" not in tool_config
and "url" not in tool_config
):
st.error(
f"'{tool_name}' 도ꡬ μ„€μ •μ—λŠ” 'command' λ˜λŠ” 'url' ν•„λ“œκ°€ ν•„μš”ν•©λ‹ˆλ‹€."
)
elif "command" in tool_config and "args" not in tool_config:
st.error(
f"'{tool_name}' 도ꡬ μ„€μ •μ—λŠ” 'args' ν•„λ“œκ°€ ν•„μš”ν•©λ‹ˆλ‹€."
)
elif "command" in tool_config and not isinstance(
tool_config["args"], list
):
st.error(
f"'{tool_name}' λ„κ΅¬μ˜ 'args' ν•„λ“œλŠ” λ°˜λ“œμ‹œ λ°°μ—΄([]) ν˜•μ‹μ΄μ–΄μ•Ό ν•©λ‹ˆλ‹€."
)
else:
# pending_mcp_config에 도ꡬ μΆ”κ°€
st.session_state.pending_mcp_config[tool_name] = (
tool_config
)
success_tools.append(tool_name)
# 성곡 λ©”μ‹œμ§€
if success_tools:
if len(success_tools) == 1:
st.success(
f"{success_tools[0]} 도ꡬ가 μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€. μ μš©ν•˜λ €λ©΄ 'μ„€μ • μ μš©ν•˜κΈ°' λ²„νŠΌμ„ λˆŒλŸ¬μ£Όμ„Έμš”."
)
else:
tool_names = ", ".join(success_tools)
st.success(
f"총 {len(success_tools)}개 도ꡬ({tool_names})κ°€ μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€. μ μš©ν•˜λ €λ©΄ 'μ„€μ • μ μš©ν•˜κΈ°' λ²„νŠΌμ„ λˆŒλŸ¬μ£Όμ„Έμš”."
)
# μΆ”κ°€λ˜λ©΄ expanderλ₯Ό μ ‘μ–΄μ€Œ
st.session_state.mcp_tools_expander = False
st.rerun()
except json.JSONDecodeError as e:
st.error(f"JSON νŒŒμ‹± μ—λŸ¬: {e}")
st.markdown(
f"""
**μˆ˜μ • 방법**:
1. JSON ν˜•μ‹μ΄ μ˜¬λ°”λ₯Έμ§€ ν™•μΈν•˜μ„Έμš”.
2. λͺ¨λ“  ν‚€λŠ” ν°λ”°μ˜΄ν‘œ(")둜 감싸야 ν•©λ‹ˆλ‹€.
3. λ¬Έμžμ—΄ 값도 ν°λ”°μ˜΄ν‘œ(")둜 감싸야 ν•©λ‹ˆλ‹€.
4. λ¬Έμžμ—΄ λ‚΄μ—μ„œ ν°λ”°μ˜΄ν‘œλ₯Ό μ‚¬μš©ν•  경우 μ΄μŠ€μΌ€μ΄ν”„(\\")ν•΄μ•Ό ν•©λ‹ˆλ‹€.
"""
)
except Exception as e:
st.error(f"였λ₯˜ λ°œμƒ: {e}")
# λ“±λ‘λœ 도ꡬ λͺ©λ‘ ν‘œμ‹œ 및 μ‚­μ œ λ²„νŠΌ μΆ”κ°€
with st.expander("πŸ“‹ λ“±λ‘λœ 도ꡬ λͺ©λ‘", expanded=True):
try:
pending_config = st.session_state.pending_mcp_config
except Exception as e:
st.error("μœ νš¨ν•œ MCP 도ꡬ 섀정이 μ•„λ‹™λ‹ˆλ‹€.")
else:
# pending config의 ν‚€(도ꡬ 이름) λͺ©λ‘μ„ μˆœνšŒν•˜λ©° ν‘œμ‹œ
for tool_name in list(pending_config.keys()):
col1, col2 = st.columns([8, 2])
col1.markdown(f"- **{tool_name}**")
if col2.button("μ‚­μ œ", key=f"delete_{tool_name}"):
# pending configμ—μ„œ ν•΄λ‹Ή 도ꡬ μ‚­μ œ (μ¦‰μ‹œ μ μš©λ˜μ§€λŠ” μ•ŠμŒ)
del st.session_state.pending_mcp_config[tool_name]
st.success(
f"{tool_name} 도ꡬ가 μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. μ μš©ν•˜λ €λ©΄ 'μ„€μ • μ μš©ν•˜κΈ°' λ²„νŠΌμ„ λˆŒλŸ¬μ£Όμ„Έμš”."
)
st.divider() # ꡬ뢄선 μΆ”κ°€
# --- μ‚¬μ΄λ“œλ°”: μ‹œμŠ€ν…œ 정보 및 μž‘μ—… λ²„νŠΌ μ„Ήμ…˜ ---
with st.sidebar:
st.subheader("πŸ“Š μ‹œμŠ€ν…œ 정보")
st.write(f"πŸ› οΈ MCP 도ꡬ 수: {st.session_state.get('tool_count', 'μ΄ˆκΈ°ν™” 쀑...')}")
selected_model_name = st.session_state.selected_model
st.write(f"🧠 ν˜„μž¬ λͺ¨λΈ: {selected_model_name}")
# μ„€μ • μ μš©ν•˜κΈ° λ²„νŠΌμ„ μ—¬κΈ°λ‘œ 이동
if st.button(
"μ„€μ • μ μš©ν•˜κΈ°",
key="apply_button",
type="primary",
use_container_width=True,
):
# 적용 쀑 λ©”μ‹œμ§€ ν‘œμ‹œ
apply_status = st.empty()
with apply_status.container():
st.warning("πŸ”„ 변경사항을 μ μš©ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. μž μ‹œλ§Œ κΈ°λ‹€λ €μ£Όμ„Έμš”...")
progress_bar = st.progress(0)
# μ„€μ • μ €μž₯
st.session_state.mcp_config_text = json.dumps(
st.session_state.pending_mcp_config, indent=2, ensure_ascii=False
)
# config.json νŒŒμΌμ— μ„€μ • μ €μž₯
save_result = save_config_to_json(st.session_state.pending_mcp_config)
if not save_result:
st.error("❌ μ„€μ • 파일 μ €μž₯에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.")
progress_bar.progress(15)
# μ„Έμ…˜ μ΄ˆκΈ°ν™” μ€€λΉ„
st.session_state.session_initialized = False
st.session_state.agent = None
# μ§„ν–‰ μƒνƒœ μ—…λ°μ΄νŠΈ
progress_bar.progress(30)
# μ΄ˆκΈ°ν™” μ‹€ν–‰
success = st.session_state.event_loop.run_until_complete(
initialize_session(st.session_state.pending_mcp_config)
)
# μ§„ν–‰ μƒνƒœ μ—…λ°μ΄νŠΈ
progress_bar.progress(100)
if success:
st.success("βœ… μƒˆλ‘œμš΄ 섀정이 μ μš©λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
# 도ꡬ μΆ”κ°€ expander μ ‘κΈ°
if "mcp_tools_expander" in st.session_state:
st.session_state.mcp_tools_expander = False
else:
st.error("❌ μ„€μ • μ μš©μ— μ‹€νŒ¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€.")
# νŽ˜μ΄μ§€ μƒˆλ‘œκ³ μΉ¨
st.rerun()
st.divider() # ꡬ뢄선 μΆ”κ°€
# μž‘μ—… λ²„νŠΌ μ„Ήμ…˜
st.subheader("πŸ”„ μž‘μ—…")
# λŒ€ν™” μ΄ˆκΈ°ν™” λ²„νŠΌ
if st.button("λŒ€ν™” μ΄ˆκΈ°ν™”", use_container_width=True, type="primary"):
# thread_id μ΄ˆκΈ°ν™”
st.session_state.thread_id = random_uuid()
# λŒ€ν™” νžˆμŠ€ν† λ¦¬ μ΄ˆκΈ°ν™”
st.session_state.history = []
# μ•Œλ¦Ό λ©”μ‹œμ§€
st.success("βœ… λŒ€ν™”κ°€ μ΄ˆκΈ°ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
# νŽ˜μ΄μ§€ μƒˆλ‘œκ³ μΉ¨
st.rerun()
# 둜그인 κΈ°λŠ₯이 ν™œμ„±ν™”λœ κ²½μš°μ—λ§Œ λ‘œκ·Έμ•„μ›ƒ λ²„νŠΌ ν‘œμ‹œ
if use_login and st.session_state.authenticated:
st.divider() # ꡬ뢄선 μΆ”κ°€
if st.button("λ‘œκ·Έμ•„μ›ƒ", use_container_width=True, type="secondary"):
st.session_state.authenticated = False
st.success("βœ… λ‘œκ·Έμ•„μ›ƒ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
st.rerun()
# --- κΈ°λ³Έ μ„Έμ…˜ μ΄ˆκΈ°ν™” (μ΄ˆκΈ°ν™”λ˜μ§€ μ•Šμ€ 경우) ---
if not st.session_state.session_initialized:
st.info(
"MCP μ„œλ²„μ™€ μ—μ΄μ „νŠΈκ°€ μ΄ˆκΈ°ν™”λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. μ™Όμͺ½ μ‚¬μ΄λ“œλ°”μ˜ 'μ„€μ • μ μš©ν•˜κΈ°' λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ μ΄ˆκΈ°ν™”ν•΄μ£Όμ„Έμš”."
)
# --- λŒ€ν™” 기둝 좜λ ₯ ---
print_message()
# --- μ‚¬μš©μž μž…λ ₯ 및 처리 ---
user_query = st.chat_input("πŸ’¬ μ§ˆλ¬Έμ„ μž…λ ₯ν•˜μ„Έμš”")
if user_query:
if st.session_state.session_initialized:
st.chat_message("user", avatar="πŸ§‘β€πŸ’»").markdown(user_query)
with st.chat_message("assistant", avatar="πŸ€–"):
tool_placeholder = st.empty()
text_placeholder = st.empty()
resp, final_text, final_tool = (
st.session_state.event_loop.run_until_complete(
process_query(
user_query,
text_placeholder,
tool_placeholder,
st.session_state.timeout_seconds,
)
)
)
if "error" in resp:
st.error(resp["error"])
else:
st.session_state.history.append({"role": "user", "content": user_query})
st.session_state.history.append(
{"role": "assistant", "content": final_text}
)
if final_tool.strip():
st.session_state.history.append(
{"role": "assistant_tool", "content": final_tool}
)
st.rerun()
else:
st.warning(
"⚠️ MCP μ„œλ²„μ™€ μ—μ΄μ „νŠΈκ°€ μ΄ˆκΈ°ν™”λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. μ™Όμͺ½ μ‚¬μ΄λ“œλ°”μ˜ 'μ„€μ • μ μš©ν•˜κΈ°' λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ μ΄ˆκΈ°ν™”ν•΄μ£Όμ„Έμš”."
)