Spaces:
Sleeping
Sleeping
fix: track PNGs with LFS
Browse files- .gitattributes +1 -0
- MCP-HandsOn-ENG.ipynb +702 -0
- MCP-HandsOn-KOR.ipynb +701 -0
- README.md +224 -13
- README_KOR.md +232 -0
- app.py +865 -0
- app_KOR.py +848 -0
- assets/add-tools.png +3 -0
- assets/apply-tool-configuration.png +3 -0
- assets/architecture.png +3 -0
- assets/check-status.png +3 -0
- assets/project-demo.png +3 -0
- assets/smithery-copy-json.png +3 -0
- assets/smithery-json.png +3 -0
- config.json +9 -0
- dockers/.env.example +10 -0
- dockers/config.json +9 -0
- dockers/docker-compose-KOR-mac.yaml +36 -0
- dockers/docker-compose-KOR.yaml +33 -0
- dockers/docker-compose-mac.yaml +36 -0
- dockers/docker-compose.yaml +34 -0
- example_config.json +7 -0
- mcp_server_local.py +36 -0
- mcp_server_rag.py +89 -0
- mcp_server_remote.py +37 -0
- mcp_server_time.py +50 -0
- packages.txt +5 -0
- pyproject.toml +21 -0
- requirements.txt +13 -0
- utils.py +322 -0
- uv.lock +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/*.png filter=lfs diff=lfs merge=lfs -text
|
MCP-HandsOn-ENG.ipynb
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"metadata": {},
|
| 6 |
+
"source": [
|
| 7 |
+
"# MCP + LangGraph Hands-On Tutorial\n",
|
| 8 |
+
"\n",
|
| 9 |
+
"- Author: [Teddy Notes](https://youtube.com/c/teddynote)\n",
|
| 10 |
+
"- Lecture: [Fastcampus RAG trick notes](https://fastcampus.co.kr/data_online_teddy)\n",
|
| 11 |
+
"\n",
|
| 12 |
+
"**References**\n",
|
| 13 |
+
"- https://modelcontextprotocol.io/introduction\n",
|
| 14 |
+
"- https://github.com/langchain-ai/langchain-mcp-adapters"
|
| 15 |
+
]
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"cell_type": "markdown",
|
| 19 |
+
"metadata": {},
|
| 20 |
+
"source": [
|
| 21 |
+
"## configure\n",
|
| 22 |
+
"\n",
|
| 23 |
+
"Refer to the installation instructions below to install `uv`.\n",
|
| 24 |
+
"\n",
|
| 25 |
+
"**How to install `uv`**\n",
|
| 26 |
+
"\n",
|
| 27 |
+
"```bash\n",
|
| 28 |
+
"# macOS/Linux\n",
|
| 29 |
+
"curl -LsSf https://astral.sh/uv/install.sh | sh\n",
|
| 30 |
+
"\n",
|
| 31 |
+
"# Windows (PowerShell)\n",
|
| 32 |
+
"irm https://astral.sh/uv/install.ps1 | iex\n",
|
| 33 |
+
"```\n",
|
| 34 |
+
"\n",
|
| 35 |
+
"Install **dependencies**\n",
|
| 36 |
+
"\n",
|
| 37 |
+
"```bash\n",
|
| 38 |
+
"uv pip install -r requirements.txt\n",
|
| 39 |
+
"```"
|
| 40 |
+
]
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"cell_type": "markdown",
|
| 44 |
+
"metadata": {},
|
| 45 |
+
"source": [
|
| 46 |
+
"Gets the environment variables."
|
| 47 |
+
]
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"cell_type": "code",
|
| 51 |
+
"execution_count": null,
|
| 52 |
+
"metadata": {},
|
| 53 |
+
"outputs": [],
|
| 54 |
+
"source": [
|
| 55 |
+
"from dotenv import load_dotenv\n",
|
| 56 |
+
"\n",
|
| 57 |
+
"load_dotenv(override=True)"
|
| 58 |
+
]
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"cell_type": "markdown",
|
| 62 |
+
"metadata": {},
|
| 63 |
+
"source": [
|
| 64 |
+
"## MultiServerMCPClient"
|
| 65 |
+
]
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"cell_type": "markdown",
|
| 69 |
+
"metadata": {},
|
| 70 |
+
"source": [
|
| 71 |
+
"Run `mcp_server_remote.py` in advance. Open a terminal with the virtual environment activated and run the server.\n",
|
| 72 |
+
"\n",
|
| 73 |
+
"> Command\n",
|
| 74 |
+
"```bash\n",
|
| 75 |
+
"source .venv/bin/activate\n",
|
| 76 |
+
"python mcp_server_remote.py\n",
|
| 77 |
+
"```\n",
|
| 78 |
+
"\n",
|
| 79 |
+
"Create and terminate a temporary Session connection using `async with`"
|
| 80 |
+
]
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"cell_type": "code",
|
| 84 |
+
"execution_count": null,
|
| 85 |
+
"metadata": {},
|
| 86 |
+
"outputs": [],
|
| 87 |
+
"source": [
|
| 88 |
+
"from langchain_mcp_adapters.client import MultiServerMCPClient\n",
|
| 89 |
+
"from langgraph.prebuilt import create_react_agent\n",
|
| 90 |
+
"from utils import ainvoke_graph, astream_graph\n",
|
| 91 |
+
"from langchain_anthropic import ChatAnthropic\n",
|
| 92 |
+
"\n",
|
| 93 |
+
"model = ChatAnthropic(\n",
|
| 94 |
+
" model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n",
|
| 95 |
+
")\n",
|
| 96 |
+
"\n",
|
| 97 |
+
"async with MultiServerMCPClient(\n",
|
| 98 |
+
" {\n",
|
| 99 |
+
" \"weather\": {\n",
|
| 100 |
+
" # Must match the server's port (port 8005)\n",
|
| 101 |
+
" \"url\": \"http://localhost:8005/sse\",\n",
|
| 102 |
+
" \"transport\": \"sse\",\n",
|
| 103 |
+
" }\n",
|
| 104 |
+
" }\n",
|
| 105 |
+
") as client:\n",
|
| 106 |
+
" print(client.get_tools())\n",
|
| 107 |
+
" agent = create_react_agent(model, client.get_tools())\n",
|
| 108 |
+
" answer = await astream_graph(\n",
|
| 109 |
+
" agent, {\"messages\": \"What's the weather like in Seoul?\"}\n",
|
| 110 |
+
" )"
|
| 111 |
+
]
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"cell_type": "markdown",
|
| 115 |
+
"metadata": {},
|
| 116 |
+
"source": [
|
| 117 |
+
"You might notice that you can't access the tool because the session is closed."
|
| 118 |
+
]
|
| 119 |
+
},
|
| 120 |
+
{
|
| 121 |
+
"cell_type": "code",
|
| 122 |
+
"execution_count": null,
|
| 123 |
+
"metadata": {},
|
| 124 |
+
"outputs": [],
|
| 125 |
+
"source": [
|
| 126 |
+
"await astream_graph(agent, {\"messages\": \"What's the weather like in Seoul?\"})"
|
| 127 |
+
]
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
"cell_type": "markdown",
|
| 131 |
+
"metadata": {},
|
| 132 |
+
"source": [
|
| 133 |
+
"Now let's change that to accessing the tool while maintaining an Async Session."
|
| 134 |
+
]
|
| 135 |
+
},
|
| 136 |
+
{
|
| 137 |
+
"cell_type": "code",
|
| 138 |
+
"execution_count": null,
|
| 139 |
+
"metadata": {},
|
| 140 |
+
"outputs": [],
|
| 141 |
+
"source": [
|
| 142 |
+
"# 1. Create client\n",
|
| 143 |
+
"client = MultiServerMCPClient(\n",
|
| 144 |
+
" {\n",
|
| 145 |
+
" \"weather\": {\n",
|
| 146 |
+
" \"url\": \"http://localhost:8005/sse\",\n",
|
| 147 |
+
" \"transport\": \"sse\",\n",
|
| 148 |
+
" }\n",
|
| 149 |
+
" }\n",
|
| 150 |
+
")\n",
|
| 151 |
+
"\n",
|
| 152 |
+
"\n",
|
| 153 |
+
"# 2. Explicitly initialize connection (this part is necessary)\n",
|
| 154 |
+
"# Initialize\n",
|
| 155 |
+
"await client.__aenter__()\n",
|
| 156 |
+
"\n",
|
| 157 |
+
"# Now tools are loaded\n",
|
| 158 |
+
"print(client.get_tools()) # Tools are displayed"
|
| 159 |
+
]
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
"cell_type": "markdown",
|
| 163 |
+
"metadata": {},
|
| 164 |
+
"source": [
|
| 165 |
+
"Create an agent with langgraph(`create_react_agent`)."
|
| 166 |
+
]
|
| 167 |
+
},
|
| 168 |
+
{
|
| 169 |
+
"cell_type": "code",
|
| 170 |
+
"execution_count": 5,
|
| 171 |
+
"metadata": {},
|
| 172 |
+
"outputs": [],
|
| 173 |
+
"source": [
|
| 174 |
+
"# Create agent\n",
|
| 175 |
+
"agent = create_react_agent(model, client.get_tools())"
|
| 176 |
+
]
|
| 177 |
+
},
|
| 178 |
+
{
|
| 179 |
+
"cell_type": "markdown",
|
| 180 |
+
"metadata": {},
|
| 181 |
+
"source": [
|
| 182 |
+
"Run the graph to see the results."
|
| 183 |
+
]
|
| 184 |
+
},
|
| 185 |
+
{
|
| 186 |
+
"cell_type": "code",
|
| 187 |
+
"execution_count": null,
|
| 188 |
+
"metadata": {},
|
| 189 |
+
"outputs": [],
|
| 190 |
+
"source": [
|
| 191 |
+
"await astream_graph(agent, {\"messages\": \"What's the weather like in Seoul?\"})"
|
| 192 |
+
]
|
| 193 |
+
},
|
| 194 |
+
{
|
| 195 |
+
"cell_type": "markdown",
|
| 196 |
+
"metadata": {},
|
| 197 |
+
"source": [
|
| 198 |
+
"## Stdio method\n",
|
| 199 |
+
"\n",
|
| 200 |
+
"The Stdio method is intended for use in a local environment.\n",
|
| 201 |
+
"\n",
|
| 202 |
+
"- Use standard input/output for communication"
|
| 203 |
+
]
|
| 204 |
+
},
|
| 205 |
+
{
|
| 206 |
+
"cell_type": "code",
|
| 207 |
+
"execution_count": null,
|
| 208 |
+
"metadata": {},
|
| 209 |
+
"outputs": [],
|
| 210 |
+
"source": [
|
| 211 |
+
"from mcp import ClientSession, StdioServerParameters\n",
|
| 212 |
+
"from mcp.client.stdio import stdio_client\n",
|
| 213 |
+
"from langgraph.prebuilt import create_react_agent\n",
|
| 214 |
+
"from langchain_mcp_adapters.tools import load_mcp_tools\n",
|
| 215 |
+
"from langchain_anthropic import ChatAnthropic\n",
|
| 216 |
+
"\n",
|
| 217 |
+
"# Initialize Anthropic's Claude model\n",
|
| 218 |
+
"model = ChatAnthropic(\n",
|
| 219 |
+
" model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n",
|
| 220 |
+
")\n",
|
| 221 |
+
"\n",
|
| 222 |
+
"# Set up StdIO server parameters\n",
|
| 223 |
+
"# - command: Path to Python interpreter\n",
|
| 224 |
+
"# - args: MCP server script to execute\n",
|
| 225 |
+
"server_params = StdioServerParameters(\n",
|
| 226 |
+
" command=\"./.venv/bin/python\",\n",
|
| 227 |
+
" args=[\"mcp_server_local.py\"],\n",
|
| 228 |
+
")\n",
|
| 229 |
+
"\n",
|
| 230 |
+
"# Use StdIO client to communicate with the server\n",
|
| 231 |
+
"async with stdio_client(server_params) as (read, write):\n",
|
| 232 |
+
" # Create client session\n",
|
| 233 |
+
" async with ClientSession(read, write) as session:\n",
|
| 234 |
+
" # Initialize connection\n",
|
| 235 |
+
" await session.initialize()\n",
|
| 236 |
+
"\n",
|
| 237 |
+
" # Load MCP tools\n",
|
| 238 |
+
" tools = await load_mcp_tools(session)\n",
|
| 239 |
+
" print(tools)\n",
|
| 240 |
+
"\n",
|
| 241 |
+
" # Create agent\n",
|
| 242 |
+
" agent = create_react_agent(model, tools)\n",
|
| 243 |
+
"\n",
|
| 244 |
+
" # Stream agent responses\n",
|
| 245 |
+
" await astream_graph(agent, {\"messages\": \"What's the weather like in Seoul?\"})"
|
| 246 |
+
]
|
| 247 |
+
},
|
| 248 |
+
{
|
| 249 |
+
"cell_type": "markdown",
|
| 250 |
+
"metadata": {},
|
| 251 |
+
"source": [
|
| 252 |
+
"## Use MCP server with RAG deployed\n",
|
| 253 |
+
"\n",
|
| 254 |
+
"- File: `mcp_server_rag.py`\n",
|
| 255 |
+
"\n",
|
| 256 |
+
"Use the `mcp_server_rag.py` file that we built with langchain in advance.\n",
|
| 257 |
+
"\n",
|
| 258 |
+
"It uses stdio communication to get information about the tools, where it gets the `retriever` tool, which is the tool defined in `mcp_server_rag.py`. This file **doesn't** need to be running on the server beforehand."
|
| 259 |
+
]
|
| 260 |
+
},
|
| 261 |
+
{
|
| 262 |
+
"cell_type": "code",
|
| 263 |
+
"execution_count": null,
|
| 264 |
+
"metadata": {},
|
| 265 |
+
"outputs": [],
|
| 266 |
+
"source": [
|
| 267 |
+
"from mcp import ClientSession, StdioServerParameters\n",
|
| 268 |
+
"from mcp.client.stdio import stdio_client\n",
|
| 269 |
+
"from langchain_mcp_adapters.tools import load_mcp_tools\n",
|
| 270 |
+
"from langgraph.prebuilt import create_react_agent\n",
|
| 271 |
+
"from langchain_anthropic import ChatAnthropic\n",
|
| 272 |
+
"from utils import astream_graph\n",
|
| 273 |
+
"\n",
|
| 274 |
+
"# Initialize Anthropic's Claude model\n",
|
| 275 |
+
"model = ChatAnthropic(\n",
|
| 276 |
+
" model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n",
|
| 277 |
+
")\n",
|
| 278 |
+
"\n",
|
| 279 |
+
"# Set up StdIO server parameters for the RAG server\n",
|
| 280 |
+
"server_params = StdioServerParameters(\n",
|
| 281 |
+
" command=\"./.venv/bin/python\",\n",
|
| 282 |
+
" args=[\"./mcp_server_rag.py\"],\n",
|
| 283 |
+
")\n",
|
| 284 |
+
"\n",
|
| 285 |
+
"# Use StdIO client to communicate with the RAG server\n",
|
| 286 |
+
"async with stdio_client(server_params) as (read, write):\n",
|
| 287 |
+
" # Create client session\n",
|
| 288 |
+
" async with ClientSession(read, write) as session:\n",
|
| 289 |
+
" # Initialize connection\n",
|
| 290 |
+
" await session.initialize()\n",
|
| 291 |
+
"\n",
|
| 292 |
+
" # Load MCP tools (in this case, the retriever tool)\n",
|
| 293 |
+
" tools = await load_mcp_tools(session)\n",
|
| 294 |
+
"\n",
|
| 295 |
+
" # Create and run the agent\n",
|
| 296 |
+
" agent = create_react_agent(model, tools)\n",
|
| 297 |
+
"\n",
|
| 298 |
+
" # Stream agent responses\n",
|
| 299 |
+
" await astream_graph(\n",
|
| 300 |
+
" agent,\n",
|
| 301 |
+
" {\n",
|
| 302 |
+
" \"messages\": \"Search for the name of the generative AI developed by Samsung Electronics\"\n",
|
| 303 |
+
" },\n",
|
| 304 |
+
" )"
|
| 305 |
+
]
|
| 306 |
+
},
|
| 307 |
+
{
|
| 308 |
+
"cell_type": "markdown",
|
| 309 |
+
"metadata": {},
|
| 310 |
+
"source": [
|
| 311 |
+
"## Use a mix of SSE and Stdio methods\n",
|
| 312 |
+
"\n",
|
| 313 |
+
"- File: `mcp_server_rag.py` communicates over Stdio\n",
|
| 314 |
+
"- `langchain-dev-docs` communicates via SSE\n",
|
| 315 |
+
"\n",
|
| 316 |
+
"Use a mix of SSE and Stdio methods."
|
| 317 |
+
]
|
| 318 |
+
},
|
| 319 |
+
{
|
| 320 |
+
"cell_type": "code",
|
| 321 |
+
"execution_count": null,
|
| 322 |
+
"metadata": {},
|
| 323 |
+
"outputs": [],
|
| 324 |
+
"source": [
|
| 325 |
+
"from langchain_mcp_adapters.client import MultiServerMCPClient\n",
|
| 326 |
+
"from langgraph.prebuilt import create_react_agent\n",
|
| 327 |
+
"from langchain_anthropic import ChatAnthropic\n",
|
| 328 |
+
"\n",
|
| 329 |
+
"# Initialize Anthropic's Claude model\n",
|
| 330 |
+
"model = ChatAnthropic(\n",
|
| 331 |
+
" model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n",
|
| 332 |
+
")\n",
|
| 333 |
+
"\n",
|
| 334 |
+
"# 1. Create multi-server MCP client\n",
|
| 335 |
+
"client = MultiServerMCPClient(\n",
|
| 336 |
+
" {\n",
|
| 337 |
+
" \"document-retriever\": {\n",
|
| 338 |
+
" \"command\": \"./.venv/bin/python\",\n",
|
| 339 |
+
" # Update with the absolute path to mcp_server_rag.py file\n",
|
| 340 |
+
" \"args\": [\"./mcp_server_rag.py\"],\n",
|
| 341 |
+
" # Communicate via stdio (using standard input/output)\n",
|
| 342 |
+
" \"transport\": \"stdio\",\n",
|
| 343 |
+
" },\n",
|
| 344 |
+
" \"langchain-dev-docs\": {\n",
|
| 345 |
+
" # Make sure the SSE server is running\n",
|
| 346 |
+
" \"url\": \"https://teddynote.io/mcp/langchain/sse\",\n",
|
| 347 |
+
" # Communicate via SSE (Server-Sent Events)\n",
|
| 348 |
+
" \"transport\": \"sse\",\n",
|
| 349 |
+
" },\n",
|
| 350 |
+
" }\n",
|
| 351 |
+
")\n",
|
| 352 |
+
"\n",
|
| 353 |
+
"\n",
|
| 354 |
+
"# 2. Initialize connection explicitly through async context manager\n",
|
| 355 |
+
"await client.__aenter__()"
|
| 356 |
+
]
|
| 357 |
+
},
|
| 358 |
+
{
|
| 359 |
+
"cell_type": "markdown",
|
| 360 |
+
"metadata": {},
|
| 361 |
+
"source": [
|
| 362 |
+
"Create an agent using `create_react_agent` in langgraph."
|
| 363 |
+
]
|
| 364 |
+
},
|
| 365 |
+
{
|
| 366 |
+
"cell_type": "code",
|
| 367 |
+
"execution_count": 10,
|
| 368 |
+
"metadata": {},
|
| 369 |
+
"outputs": [],
|
| 370 |
+
"source": [
|
| 371 |
+
"from langgraph.checkpoint.memory import MemorySaver\n",
|
| 372 |
+
"from langchain_core.runnables import RunnableConfig\n",
|
| 373 |
+
"\n",
|
| 374 |
+
"prompt = (\n",
|
| 375 |
+
" \"You are a smart agent. \"\n",
|
| 376 |
+
" \"Use `retriever` tool to search on AI related documents and answer questions.\"\n",
|
| 377 |
+
" \"Use `langchain-dev-docs` tool to search on langchain / langgraph related documents and answer questions.\"\n",
|
| 378 |
+
" \"Answer in English.\"\n",
|
| 379 |
+
")\n",
|
| 380 |
+
"agent = create_react_agent(\n",
|
| 381 |
+
" model, client.get_tools(), prompt=prompt, checkpointer=MemorySaver()\n",
|
| 382 |
+
")"
|
| 383 |
+
]
|
| 384 |
+
},
|
| 385 |
+
{
|
| 386 |
+
"cell_type": "markdown",
|
| 387 |
+
"metadata": {},
|
| 388 |
+
"source": [
|
| 389 |
+
"Use the `retriever` tool defined in `mcp_server_rag.py` that you built to perform the search."
|
| 390 |
+
]
|
| 391 |
+
},
|
| 392 |
+
{
|
| 393 |
+
"cell_type": "code",
|
| 394 |
+
"execution_count": null,
|
| 395 |
+
"metadata": {},
|
| 396 |
+
"outputs": [],
|
| 397 |
+
"source": [
|
| 398 |
+
"config = RunnableConfig(recursion_limit=30, thread_id=1)\n",
|
| 399 |
+
"await astream_graph(\n",
|
| 400 |
+
" agent,\n",
|
| 401 |
+
" {\n",
|
| 402 |
+
" \"messages\": \"Use the `retriever` tool to search for the name of the generative AI developed by Samsung Electronics\"\n",
|
| 403 |
+
" },\n",
|
| 404 |
+
" config=config,\n",
|
| 405 |
+
")"
|
| 406 |
+
]
|
| 407 |
+
},
|
| 408 |
+
{
|
| 409 |
+
"cell_type": "markdown",
|
| 410 |
+
"metadata": {},
|
| 411 |
+
"source": [
|
| 412 |
+
"This time, we'll use the `langchain-dev-docs` tool to perform the search."
|
| 413 |
+
]
|
| 414 |
+
},
|
| 415 |
+
{
|
| 416 |
+
"cell_type": "code",
|
| 417 |
+
"execution_count": null,
|
| 418 |
+
"metadata": {},
|
| 419 |
+
"outputs": [],
|
| 420 |
+
"source": [
|
| 421 |
+
"config = RunnableConfig(recursion_limit=30, thread_id=1)\n",
|
| 422 |
+
"await astream_graph(\n",
|
| 423 |
+
" agent,\n",
|
| 424 |
+
" {\n",
|
| 425 |
+
" \"messages\": \"Please tell me about the definition of self-rag by referring to the langchain-dev-docs\"\n",
|
| 426 |
+
" },\n",
|
| 427 |
+
" config=config,\n",
|
| 428 |
+
")"
|
| 429 |
+
]
|
| 430 |
+
},
|
| 431 |
+
{
|
| 432 |
+
"cell_type": "markdown",
|
| 433 |
+
"metadata": {},
|
| 434 |
+
"source": [
|
| 435 |
+
"Use `MemorySaver` to maintain short-term memory, so multi-turn conversations are possible."
|
| 436 |
+
]
|
| 437 |
+
},
|
| 438 |
+
{
|
| 439 |
+
"cell_type": "code",
|
| 440 |
+
"execution_count": null,
|
| 441 |
+
"metadata": {},
|
| 442 |
+
"outputs": [],
|
| 443 |
+
"source": [
|
| 444 |
+
"await astream_graph(\n",
|
| 445 |
+
" agent,\n",
|
| 446 |
+
" {\"messages\": \"Summarize the previous content in bullet points\"},\n",
|
| 447 |
+
" config=config,\n",
|
| 448 |
+
")"
|
| 449 |
+
]
|
| 450 |
+
},
|
| 451 |
+
{
|
| 452 |
+
"cell_type": "markdown",
|
| 453 |
+
"metadata": {},
|
| 454 |
+
"source": [
|
| 455 |
+
"## LangChain-integrated tools + MCP tools\n",
|
| 456 |
+
"\n",
|
| 457 |
+
"Here we confirm that tools integrated into LangChain can be used in conjunction with existing MCP-only tools."
|
| 458 |
+
]
|
| 459 |
+
},
|
| 460 |
+
{
|
| 461 |
+
"cell_type": "code",
|
| 462 |
+
"execution_count": 15,
|
| 463 |
+
"metadata": {},
|
| 464 |
+
"outputs": [],
|
| 465 |
+
"source": [
|
| 466 |
+
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
|
| 467 |
+
"\n",
|
| 468 |
+
"# Initialize the Tavily search tool (news type, news from the last 3 days)\n",
|
| 469 |
+
"tavily = TavilySearchResults(max_results=3, topic=\"news\", days=3)\n",
|
| 470 |
+
"\n",
|
| 471 |
+
"# Use it together with existing MCP tools\n",
|
| 472 |
+
"tools = client.get_tools() + [tavily]"
|
| 473 |
+
]
|
| 474 |
+
},
|
| 475 |
+
{
|
| 476 |
+
"cell_type": "markdown",
|
| 477 |
+
"metadata": {},
|
| 478 |
+
"source": [
|
| 479 |
+
"Create an agent using `create_react_agent` in langgraph."
|
| 480 |
+
]
|
| 481 |
+
},
|
| 482 |
+
{
|
| 483 |
+
"cell_type": "code",
|
| 484 |
+
"execution_count": 16,
|
| 485 |
+
"metadata": {},
|
| 486 |
+
"outputs": [],
|
| 487 |
+
"source": [
|
| 488 |
+
"from langgraph.checkpoint.memory import MemorySaver\n",
|
| 489 |
+
"from langchain_core.runnables import RunnableConfig\n",
|
| 490 |
+
"\n",
|
| 491 |
+
"prompt = \"You are a smart agent with various tools. Answer questions in English.\"\n",
|
| 492 |
+
"agent = create_react_agent(model, tools, prompt=prompt, checkpointer=MemorySaver())"
|
| 493 |
+
]
|
| 494 |
+
},
|
| 495 |
+
{
|
| 496 |
+
"cell_type": "markdown",
|
| 497 |
+
"metadata": {},
|
| 498 |
+
"source": [
|
| 499 |
+
"Perform a search using the newly added `tavily` tool."
|
| 500 |
+
]
|
| 501 |
+
},
|
| 502 |
+
{
|
| 503 |
+
"cell_type": "code",
|
| 504 |
+
"execution_count": null,
|
| 505 |
+
"metadata": {},
|
| 506 |
+
"outputs": [],
|
| 507 |
+
"source": [
|
| 508 |
+
"await astream_graph(\n",
|
| 509 |
+
" agent, {\"messages\": \"Tell me about today's news for me\"}, config=config\n",
|
| 510 |
+
")"
|
| 511 |
+
]
|
| 512 |
+
},
|
| 513 |
+
{
|
| 514 |
+
"cell_type": "markdown",
|
| 515 |
+
"metadata": {},
|
| 516 |
+
"source": [
|
| 517 |
+
"You can see that the `retriever` tool is working smoothly."
|
| 518 |
+
]
|
| 519 |
+
},
|
| 520 |
+
{
|
| 521 |
+
"cell_type": "code",
|
| 522 |
+
"execution_count": null,
|
| 523 |
+
"metadata": {},
|
| 524 |
+
"outputs": [],
|
| 525 |
+
"source": [
|
| 526 |
+
"await astream_graph(\n",
|
| 527 |
+
" agent,\n",
|
| 528 |
+
" {\n",
|
| 529 |
+
" \"messages\": \"Use the `retriever` tool to search for the name of the generative AI developed by Samsung Electronics\"\n",
|
| 530 |
+
" },\n",
|
| 531 |
+
" config=config,\n",
|
| 532 |
+
")"
|
| 533 |
+
]
|
| 534 |
+
},
|
| 535 |
+
{
|
| 536 |
+
"cell_type": "markdown",
|
| 537 |
+
"metadata": {},
|
| 538 |
+
"source": [
|
| 539 |
+
"## Smithery MCP Server\n",
|
| 540 |
+
"\n",
|
| 541 |
+
"- Link: https://smithery.ai/\n",
|
| 542 |
+
"\n",
|
| 543 |
+
"List of tools used:\n",
|
| 544 |
+
"\n",
|
| 545 |
+
"- Sequential Thinking: https://smithery.ai/server/@smithery-ai/server-sequential-thinking\n",
|
| 546 |
+
" - MCP server providing tools for dynamic and reflective problem-solving through structured thinking processes\n",
|
| 547 |
+
"- Desktop Commander: https://smithery.ai/server/@wonderwhy-er/desktop-commander\n",
|
| 548 |
+
" - Run terminal commands and manage files with various editing capabilities. Coding, shell and terminal, task automation\n",
|
| 549 |
+
"\n",
|
| 550 |
+
"**Note**\n",
|
| 551 |
+
"\n",
|
| 552 |
+
"- When importing tools provided by smithery in JSON format, you must set `\"transport\": \"stdio\"` as shown in the example below."
|
| 553 |
+
]
|
| 554 |
+
},
|
| 555 |
+
{
|
| 556 |
+
"cell_type": "code",
|
| 557 |
+
"execution_count": null,
|
| 558 |
+
"metadata": {},
|
| 559 |
+
"outputs": [],
|
| 560 |
+
"source": [
|
| 561 |
+
"from langchain_mcp_adapters.client import MultiServerMCPClient\n",
|
| 562 |
+
"from langgraph.prebuilt import create_react_agent\n",
|
| 563 |
+
"from langchain_anthropic import ChatAnthropic\n",
|
| 564 |
+
"\n",
|
| 565 |
+
"# Initialize LLM model\n",
|
| 566 |
+
"model = ChatAnthropic(model=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000)\n",
|
| 567 |
+
"\n",
|
| 568 |
+
"# 1. Create client\n",
|
| 569 |
+
"client = MultiServerMCPClient(\n",
|
| 570 |
+
" {\n",
|
| 571 |
+
" \"server-sequential-thinking\": {\n",
|
| 572 |
+
" \"command\": \"npx\",\n",
|
| 573 |
+
" \"args\": [\n",
|
| 574 |
+
" \"-y\",\n",
|
| 575 |
+
" \"@smithery/cli@latest\",\n",
|
| 576 |
+
" \"run\",\n",
|
| 577 |
+
" \"@smithery-ai/server-sequential-thinking\",\n",
|
| 578 |
+
" \"--key\",\n",
|
| 579 |
+
" \"your_smithery_api_key\",\n",
|
| 580 |
+
" ],\n",
|
| 581 |
+
" \"transport\": \"stdio\", # Add communication using stdio method\n",
|
| 582 |
+
" },\n",
|
| 583 |
+
" \"desktop-commander\": {\n",
|
| 584 |
+
" \"command\": \"npx\",\n",
|
| 585 |
+
" \"args\": [\n",
|
| 586 |
+
" \"-y\",\n",
|
| 587 |
+
" \"@smithery/cli@latest\",\n",
|
| 588 |
+
" \"run\",\n",
|
| 589 |
+
" \"@wonderwhy-er/desktop-commander\",\n",
|
| 590 |
+
" \"--key\",\n",
|
| 591 |
+
" \"your_smithery_api_key\",\n",
|
| 592 |
+
" ],\n",
|
| 593 |
+
" \"transport\": \"stdio\", # Add communication using stdio method\n",
|
| 594 |
+
" },\n",
|
| 595 |
+
" \"document-retriever\": {\n",
|
| 596 |
+
" \"command\": \"./.venv/bin/python\",\n",
|
| 597 |
+
" # Update with the absolute path to the mcp_server_rag.py file\n",
|
| 598 |
+
" \"args\": [\"./mcp_server_rag.py\"],\n",
|
| 599 |
+
" # Communication using stdio (standard input/output)\n",
|
| 600 |
+
" \"transport\": \"stdio\",\n",
|
| 601 |
+
" },\n",
|
| 602 |
+
" }\n",
|
| 603 |
+
")\n",
|
| 604 |
+
"\n",
|
| 605 |
+
"\n",
|
| 606 |
+
"# 2. Explicitly initialize connection\n",
|
| 607 |
+
"await client.__aenter__()"
|
| 608 |
+
]
|
| 609 |
+
},
|
| 610 |
+
{
|
| 611 |
+
"cell_type": "markdown",
|
| 612 |
+
"metadata": {},
|
| 613 |
+
"source": [
|
| 614 |
+
"Create an agent using `create_react_agent` in langgraph."
|
| 615 |
+
]
|
| 616 |
+
},
|
| 617 |
+
{
|
| 618 |
+
"cell_type": "code",
|
| 619 |
+
"execution_count": 23,
|
| 620 |
+
"metadata": {},
|
| 621 |
+
"outputs": [],
|
| 622 |
+
"source": [
|
| 623 |
+
"from langgraph.checkpoint.memory import MemorySaver\n",
|
| 624 |
+
"from langchain_core.runnables import RunnableConfig\n",
|
| 625 |
+
"\n",
|
| 626 |
+
"# Set up configuration\n",
|
| 627 |
+
"config = RunnableConfig(recursion_limit=30, thread_id=3)\n",
|
| 628 |
+
"\n",
|
| 629 |
+
"# Create agent\n",
|
| 630 |
+
"agent = create_react_agent(model, client.get_tools(), checkpointer=MemorySaver())"
|
| 631 |
+
]
|
| 632 |
+
},
|
| 633 |
+
{
|
| 634 |
+
"cell_type": "markdown",
|
| 635 |
+
"metadata": {},
|
| 636 |
+
"source": [
|
| 637 |
+
"`Desktop Commander` ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ฌ ํฐ๋ฏธ๋ ๋ช
๋ น์ ์คํํฉ๋๋ค."
|
| 638 |
+
]
|
| 639 |
+
},
|
| 640 |
+
{
|
| 641 |
+
"cell_type": "code",
|
| 642 |
+
"execution_count": null,
|
| 643 |
+
"metadata": {},
|
| 644 |
+
"outputs": [],
|
| 645 |
+
"source": [
|
| 646 |
+
"await astream_graph(\n",
|
| 647 |
+
" agent,\n",
|
| 648 |
+
" {\n",
|
| 649 |
+
" \"messages\": \"Draw the folder structure including the current path as a tree. However, exclude the .venv folder from the output.\"\n",
|
| 650 |
+
" },\n",
|
| 651 |
+
" config=config,\n",
|
| 652 |
+
")"
|
| 653 |
+
]
|
| 654 |
+
},
|
| 655 |
+
{
|
| 656 |
+
"cell_type": "markdown",
|
| 657 |
+
"metadata": {},
|
| 658 |
+
"source": [
|
| 659 |
+
"We'll use the `Sequential Thinking` tool to see if we can accomplish a relatively complex task."
|
| 660 |
+
]
|
| 661 |
+
},
|
| 662 |
+
{
|
| 663 |
+
"cell_type": "code",
|
| 664 |
+
"execution_count": null,
|
| 665 |
+
"metadata": {},
|
| 666 |
+
"outputs": [],
|
| 667 |
+
"source": [
|
| 668 |
+
"await astream_graph(\n",
|
| 669 |
+
" agent,\n",
|
| 670 |
+
" {\n",
|
| 671 |
+
" \"messages\": (\n",
|
| 672 |
+
" \"Use the `retriever` tool to search for information about generative AI developed by Samsung Electronics, \"\n",
|
| 673 |
+
" \"and then use the `Sequential Thinking` tool to write a report.\"\n",
|
| 674 |
+
" )\n",
|
| 675 |
+
" },\n",
|
| 676 |
+
" config=config,\n",
|
| 677 |
+
")"
|
| 678 |
+
]
|
| 679 |
+
}
|
| 680 |
+
],
|
| 681 |
+
"metadata": {
|
| 682 |
+
"kernelspec": {
|
| 683 |
+
"display_name": ".venv",
|
| 684 |
+
"language": "python",
|
| 685 |
+
"name": "python3"
|
| 686 |
+
},
|
| 687 |
+
"language_info": {
|
| 688 |
+
"codemirror_mode": {
|
| 689 |
+
"name": "ipython",
|
| 690 |
+
"version": 3
|
| 691 |
+
},
|
| 692 |
+
"file_extension": ".py",
|
| 693 |
+
"mimetype": "text/x-python",
|
| 694 |
+
"name": "python",
|
| 695 |
+
"nbconvert_exporter": "python",
|
| 696 |
+
"pygments_lexer": "ipython3",
|
| 697 |
+
"version": "3.12.8"
|
| 698 |
+
}
|
| 699 |
+
},
|
| 700 |
+
"nbformat": 4,
|
| 701 |
+
"nbformat_minor": 2
|
| 702 |
+
}
|
MCP-HandsOn-KOR.ipynb
ADDED
|
@@ -0,0 +1,701 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"metadata": {},
|
| 6 |
+
"source": [
|
| 7 |
+
"# MCP + LangGraph ํธ์ฆ์จ ํํ ๋ฆฌ์ผ\n",
|
| 8 |
+
"\n",
|
| 9 |
+
"- ์์ฑ์: [ํ
๋๋
ธํธ](https://youtube.com/c/teddynote)\n",
|
| 10 |
+
"- ๊ฐ์: [ํจ์คํธ์บ ํผ์ค RAG ๋น๋ฒ๋
ธํธ](https://fastcampus.co.kr/data_online_teddy)\n",
|
| 11 |
+
"\n",
|
| 12 |
+
"**์ฐธ๊ณ ์๋ฃ**\n",
|
| 13 |
+
"- https://modelcontextprotocol.io/introduction\n",
|
| 14 |
+
"- https://github.com/langchain-ai/langchain-mcp-adapters"
|
| 15 |
+
]
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"cell_type": "markdown",
|
| 19 |
+
"metadata": {},
|
| 20 |
+
"source": [
|
| 21 |
+
"## ํ๊ฒฝ์ค์ \n",
|
| 22 |
+
"\n",
|
| 23 |
+
"์๋ ์ค์น ๋ฐฉ๋ฒ์ ์ฐธ๊ณ ํ์ฌ `uv` ๋ฅผ ์ค์นํฉ๋๋ค.\n",
|
| 24 |
+
"\n",
|
| 25 |
+
"**uv ์ค์น ๋ฐฉ๋ฒ**\n",
|
| 26 |
+
"\n",
|
| 27 |
+
"```bash\n",
|
| 28 |
+
"# macOS/Linux\n",
|
| 29 |
+
"curl -LsSf https://astral.sh/uv/install.sh | sh\n",
|
| 30 |
+
"\n",
|
| 31 |
+
"# Windows (PowerShell)\n",
|
| 32 |
+
"irm https://astral.sh/uv/install.ps1 | iex\n",
|
| 33 |
+
"```\n",
|
| 34 |
+
"\n",
|
| 35 |
+
"**์์กด์ฑ ์ค์น**\n",
|
| 36 |
+
"\n",
|
| 37 |
+
"```bash\n",
|
| 38 |
+
"uv pip install -r requirements.txt\n",
|
| 39 |
+
"```"
|
| 40 |
+
]
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"cell_type": "markdown",
|
| 44 |
+
"metadata": {},
|
| 45 |
+
"source": [
|
| 46 |
+
"ํ๊ฒฝ๋ณ์๋ฅผ ๊ฐ์ ธ์ต๋๋ค."
|
| 47 |
+
]
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"cell_type": "code",
|
| 51 |
+
"execution_count": null,
|
| 52 |
+
"metadata": {},
|
| 53 |
+
"outputs": [],
|
| 54 |
+
"source": [
|
| 55 |
+
"from dotenv import load_dotenv\n",
|
| 56 |
+
"\n",
|
| 57 |
+
"load_dotenv(override=True)"
|
| 58 |
+
]
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"cell_type": "markdown",
|
| 62 |
+
"metadata": {},
|
| 63 |
+
"source": [
|
| 64 |
+
"## MultiServerMCPClient"
|
| 65 |
+
]
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"cell_type": "markdown",
|
| 69 |
+
"metadata": {},
|
| 70 |
+
"source": [
|
| 71 |
+
"์ฌ์ ์ `mcp_server_remote.py` ๋ฅผ ์คํํด๋ก๋๋ค. ํฐ๋ฏธ๋์ ์ด๊ณ ๊ฐ์ํ๊ฒฝ์ด ํ์ฑํ ๋์ด ์๋ ์ํ์์ ์๋ฒ๋ฅผ ์คํํด ์ฃผ์ธ์.\n",
|
| 72 |
+
"\n",
|
| 73 |
+
"> ๋ช
๋ น์ด\n",
|
| 74 |
+
"```bash\n",
|
| 75 |
+
"source .venv/bin/activate\n",
|
| 76 |
+
"python mcp_server_remote.py\n",
|
| 77 |
+
"```\n",
|
| 78 |
+
"\n",
|
| 79 |
+
"`async with` ๋ก ์ผ์์ ์ธ Session ์ฐ๊ฒฐ์ ์์ฑ ํ ํด์ "
|
| 80 |
+
]
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"cell_type": "code",
|
| 84 |
+
"execution_count": null,
|
| 85 |
+
"metadata": {},
|
| 86 |
+
"outputs": [],
|
| 87 |
+
"source": [
|
| 88 |
+
"from langchain_mcp_adapters.client import MultiServerMCPClient\n",
|
| 89 |
+
"from langgraph.prebuilt import create_react_agent\n",
|
| 90 |
+
"from utils import ainvoke_graph, astream_graph\n",
|
| 91 |
+
"from langchain_anthropic import ChatAnthropic\n",
|
| 92 |
+
"\n",
|
| 93 |
+
"model = ChatAnthropic(\n",
|
| 94 |
+
" model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n",
|
| 95 |
+
")\n",
|
| 96 |
+
"\n",
|
| 97 |
+
"async with MultiServerMCPClient(\n",
|
| 98 |
+
" {\n",
|
| 99 |
+
" \"weather\": {\n",
|
| 100 |
+
" # ์๋ฒ์ ํฌํธ์ ์ผ์นํด์ผ ํฉ๋๋ค.(8005๋ฒ ํฌํธ)\n",
|
| 101 |
+
" \"url\": \"http://localhost:8005/sse\",\n",
|
| 102 |
+
" \"transport\": \"sse\",\n",
|
| 103 |
+
" }\n",
|
| 104 |
+
" }\n",
|
| 105 |
+
") as client:\n",
|
| 106 |
+
" print(client.get_tools())\n",
|
| 107 |
+
" agent = create_react_agent(model, client.get_tools())\n",
|
| 108 |
+
" answer = await astream_graph(agent, {\"messages\": \"์์ธ์ ๋ ์จ๋ ์ด๋ ๋?\"})"
|
| 109 |
+
]
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
"cell_type": "markdown",
|
| 113 |
+
"metadata": {},
|
| 114 |
+
"source": [
|
| 115 |
+
"๋ค์์ ๊ฒฝ์ฐ์๋ session ์ด ๋ซํ๊ธฐ ๋๋ฌธ์ ๋๊ตฌ์ ์ ๊ทผํ ์ ์๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค."
|
| 116 |
+
]
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"cell_type": "code",
|
| 120 |
+
"execution_count": null,
|
| 121 |
+
"metadata": {},
|
| 122 |
+
"outputs": [],
|
| 123 |
+
"source": [
|
| 124 |
+
"await astream_graph(agent, {\"messages\": \"์์ธ์ ๋ ์จ๋ ์ด๋ ๋?\"})"
|
| 125 |
+
]
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
"cell_type": "markdown",
|
| 129 |
+
"metadata": {},
|
| 130 |
+
"source": [
|
| 131 |
+
"์ด์ ๊ทธ๋ผ Async Session ์ ์ ์งํ๋ฉฐ ๋๊ตฌ์ ์ ๊ทผํ๋ ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝํด ๋ณด๊ฒ ์ต๋๋ค."
|
| 132 |
+
]
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"cell_type": "code",
|
| 136 |
+
"execution_count": null,
|
| 137 |
+
"metadata": {},
|
| 138 |
+
"outputs": [],
|
| 139 |
+
"source": [
|
| 140 |
+
"# 1. ํด๋ผ์ด์ธํธ ์์ฑ\n",
|
| 141 |
+
"client = MultiServerMCPClient(\n",
|
| 142 |
+
" {\n",
|
| 143 |
+
" \"weather\": {\n",
|
| 144 |
+
" \"url\": \"http://localhost:8005/sse\",\n",
|
| 145 |
+
" \"transport\": \"sse\",\n",
|
| 146 |
+
" }\n",
|
| 147 |
+
" }\n",
|
| 148 |
+
")\n",
|
| 149 |
+
"\n",
|
| 150 |
+
"\n",
|
| 151 |
+
"# 2. ๋ช
์์ ์ผ๋ก ์ฐ๊ฒฐ ์ด๊ธฐํ (์ด ๋ถ๋ถ์ด ํ์ํจ)\n",
|
| 152 |
+
"# ์ด๊ธฐํ\n",
|
| 153 |
+
"await client.__aenter__()\n",
|
| 154 |
+
"\n",
|
| 155 |
+
"# ์ด์ ๋๊ตฌ๊ฐ ๋ก๋๋จ\n",
|
| 156 |
+
"print(client.get_tools()) # ๋๊ตฌ๊ฐ ํ์๋จ"
|
| 157 |
+
]
|
| 158 |
+
},
|
| 159 |
+
{
|
| 160 |
+
"cell_type": "markdown",
|
| 161 |
+
"metadata": {},
|
| 162 |
+
"source": [
|
| 163 |
+
"langgraph ์ ์์ด์ ํธ๋ฅผ ์์ฑํฉ๋๋ค."
|
| 164 |
+
]
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"cell_type": "code",
|
| 168 |
+
"execution_count": 5,
|
| 169 |
+
"metadata": {},
|
| 170 |
+
"outputs": [],
|
| 171 |
+
"source": [
|
| 172 |
+
"# ์์ด์ ํธ ์์ฑ\n",
|
| 173 |
+
"agent = create_react_agent(model, client.get_tools())"
|
| 174 |
+
]
|
| 175 |
+
},
|
| 176 |
+
{
|
| 177 |
+
"cell_type": "markdown",
|
| 178 |
+
"metadata": {},
|
| 179 |
+
"source": [
|
| 180 |
+
"๊ทธ๋ํ๋ฅผ ์คํํ์ฌ ๊ฒฐ๊ณผ๋ฅผ ํ์ธํฉ๋๋ค."
|
| 181 |
+
]
|
| 182 |
+
},
|
| 183 |
+
{
|
| 184 |
+
"cell_type": "code",
|
| 185 |
+
"execution_count": null,
|
| 186 |
+
"metadata": {},
|
| 187 |
+
"outputs": [],
|
| 188 |
+
"source": [
|
| 189 |
+
"await astream_graph(agent, {\"messages\": \"์์ธ์ ๋ ์จ๋ ์ด๋ ๋?\"})"
|
| 190 |
+
]
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
"cell_type": "markdown",
|
| 194 |
+
"metadata": {},
|
| 195 |
+
"source": [
|
| 196 |
+
"## Stdio ํต์ ๋ฐฉ์\n",
|
| 197 |
+
"\n",
|
| 198 |
+
"Stdio ํต์ ๋ฐฉ์์ ๋ก์ปฌ ํ๊ฒฝ์์ ์ฌ์ฉํ๊ธฐ ์ํด ์ฌ์ฉํฉ๋๋ค.\n",
|
| 199 |
+
"\n",
|
| 200 |
+
"- ํต์ ์ ์ํด ํ์ค ์
๋ ฅ/์ถ๋ ฅ ์ฌ์ฉ\n",
|
| 201 |
+
"\n",
|
| 202 |
+
"์ฐธ๊ณ : ์๋์ python ๊ฒฝ๋ก๋ ์์ ํ์ธ์!"
|
| 203 |
+
]
|
| 204 |
+
},
|
| 205 |
+
{
|
| 206 |
+
"cell_type": "code",
|
| 207 |
+
"execution_count": null,
|
| 208 |
+
"metadata": {},
|
| 209 |
+
"outputs": [],
|
| 210 |
+
"source": [
|
| 211 |
+
"from mcp import ClientSession, StdioServerParameters\n",
|
| 212 |
+
"from mcp.client.stdio import stdio_client\n",
|
| 213 |
+
"from langgraph.prebuilt import create_react_agent\n",
|
| 214 |
+
"from langchain_mcp_adapters.tools import load_mcp_tools\n",
|
| 215 |
+
"from langchain_anthropic import ChatAnthropic\n",
|
| 216 |
+
"\n",
|
| 217 |
+
"# Anthropic์ Claude ๋ชจ๋ธ ์ด๊ธฐํ\n",
|
| 218 |
+
"model = ChatAnthropic(\n",
|
| 219 |
+
" model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n",
|
| 220 |
+
")\n",
|
| 221 |
+
"\n",
|
| 222 |
+
"# StdIO ์๋ฒ ํ๋ผ๋ฏธํฐ ์ค์ \n",
|
| 223 |
+
"# - command: Python ์ธํฐํ๋ฆฌํฐ ๊ฒฝ๋ก\n",
|
| 224 |
+
"# - args: ์คํํ MCP ์๋ฒ ์คํฌ๋ฆฝํธ\n",
|
| 225 |
+
"server_params = StdioServerParameters(\n",
|
| 226 |
+
" command=\"./.venv/bin/python\",\n",
|
| 227 |
+
" args=[\"mcp_server_local.py\"],\n",
|
| 228 |
+
")\n",
|
| 229 |
+
"\n",
|
| 230 |
+
"# StdIO ํด๋ผ์ด์ธํธ๋ฅผ ์ฌ์ฉํ์ฌ ์๋ฒ์ ํต์ \n",
|
| 231 |
+
"async with stdio_client(server_params) as (read, write):\n",
|
| 232 |
+
" # ํด๋ผ์ด์ธํธ ์ธ์
์์ฑ\n",
|
| 233 |
+
" async with ClientSession(read, write) as session:\n",
|
| 234 |
+
" # ์ฐ๊ฒฐ ์ด๊ธฐํ\n",
|
| 235 |
+
" await session.initialize()\n",
|
| 236 |
+
"\n",
|
| 237 |
+
" # MCP ๋๊ตฌ ๋ก๋\n",
|
| 238 |
+
" tools = await load_mcp_tools(session)\n",
|
| 239 |
+
" print(tools)\n",
|
| 240 |
+
"\n",
|
| 241 |
+
" # ์์ด์ ํธ ์์ฑ\n",
|
| 242 |
+
" agent = create_react_agent(model, tools)\n",
|
| 243 |
+
"\n",
|
| 244 |
+
" # ์์ด์ ํธ ์๋ต ์คํธ๋ฆฌ๋ฐ\n",
|
| 245 |
+
" await astream_graph(agent, {\"messages\": \"์์ธ์ ๋ ์จ๋ ์ด๋ ๋?\"})"
|
| 246 |
+
]
|
| 247 |
+
},
|
| 248 |
+
{
|
| 249 |
+
"cell_type": "markdown",
|
| 250 |
+
"metadata": {},
|
| 251 |
+
"source": [
|
| 252 |
+
"## RAG ๋ฅผ ๊ตฌ์ถํ MCP ์๋ฒ ์ฌ์ฉ\n",
|
| 253 |
+
"\n",
|
| 254 |
+
"- ํ์ผ: `mcp_server_rag.py`\n",
|
| 255 |
+
"\n",
|
| 256 |
+
"์ฌ์ ์ langchain ์ผ๋ก ๊ตฌ์ถํ `mcp_server_rag.py` ํ์ผ์ ์ฌ์ฉํฉ๋๋ค.\n",
|
| 257 |
+
"\n",
|
| 258 |
+
"stdio ํต์ ๋ฐฉ์์ผ๋ก ๋๊ตฌ์ ๋ํ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ต๋๋ค. ์ฌ๊ธฐ์ ๋๊ตฌ๋ `retriever` ๋๊ตฌ๋ฅผ ๊ฐ์ ธ์ค๊ฒ ๋๋ฉฐ, ์ด ๋๊ตฌ๋ `mcp_server_rag.py` ์์ ์ ์๋ ๋๊ตฌ์
๋๋ค. ์ด ํ์ผ์ ์ฌ์ ์ ์๋ฒ์์ ์คํ๋์ง **์์๋** ๋ฉ๋๋ค."
|
| 259 |
+
]
|
| 260 |
+
},
|
| 261 |
+
{
|
| 262 |
+
"cell_type": "code",
|
| 263 |
+
"execution_count": null,
|
| 264 |
+
"metadata": {},
|
| 265 |
+
"outputs": [],
|
| 266 |
+
"source": [
|
| 267 |
+
"from mcp import ClientSession, StdioServerParameters\n",
|
| 268 |
+
"from mcp.client.stdio import stdio_client\n",
|
| 269 |
+
"from langchain_mcp_adapters.tools import load_mcp_tools\n",
|
| 270 |
+
"from langgraph.prebuilt import create_react_agent\n",
|
| 271 |
+
"from langchain_anthropic import ChatAnthropic\n",
|
| 272 |
+
"from utils import astream_graph\n",
|
| 273 |
+
"\n",
|
| 274 |
+
"# Anthropic์ Claude ๋ชจ๋ธ ์ด๊ธฐํ\n",
|
| 275 |
+
"model = ChatAnthropic(\n",
|
| 276 |
+
" model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n",
|
| 277 |
+
")\n",
|
| 278 |
+
"\n",
|
| 279 |
+
"# RAG ์๋ฒ๋ฅผ ์ํ StdIO ์๋ฒ ํ๋ผ๋ฏธํฐ ์ค์ \n",
|
| 280 |
+
"server_params = StdioServerParameters(\n",
|
| 281 |
+
" command=\"./.venv/bin/python\",\n",
|
| 282 |
+
" args=[\"./mcp_server_rag.py\"],\n",
|
| 283 |
+
")\n",
|
| 284 |
+
"\n",
|
| 285 |
+
"# StdIO ํด๋ผ์ด์ธํธ๋ฅผ ์ฌ์ฉํ์ฌ RAG ์๋ฒ์ ํต์ \n",
|
| 286 |
+
"async with stdio_client(server_params) as (read, write):\n",
|
| 287 |
+
" # ํด๋ผ์ด์ธํธ ์ธ์
์์ฑ\n",
|
| 288 |
+
" async with ClientSession(read, write) as session:\n",
|
| 289 |
+
" # ์ฐ๊ฒฐ ์ด๊ธฐํ\n",
|
| 290 |
+
" await session.initialize()\n",
|
| 291 |
+
"\n",
|
| 292 |
+
" # MCP ๋๊ตฌ ๋ก๋ (์ฌ๊ธฐ์๋ retriever ๋๊ตฌ)\n",
|
| 293 |
+
" tools = await load_mcp_tools(session)\n",
|
| 294 |
+
"\n",
|
| 295 |
+
" # ์์ด์ ํธ ์์ฑ ๋ฐ ์คํ\n",
|
| 296 |
+
" agent = create_react_agent(model, tools)\n",
|
| 297 |
+
"\n",
|
| 298 |
+
" # ์์ด์ ํธ ์๋ต ์คํธ๋ฆฌ๋ฐ\n",
|
| 299 |
+
" await astream_graph(\n",
|
| 300 |
+
" agent, {\"messages\": \"์ผ์ฑ์ ์๊ฐ ๊ฐ๋ฐํ ์์ฑํ AI์ ์ด๋ฆ์ ๊ฒ์ํด์ค\"}\n",
|
| 301 |
+
" )"
|
| 302 |
+
]
|
| 303 |
+
},
|
| 304 |
+
{
|
| 305 |
+
"cell_type": "markdown",
|
| 306 |
+
"metadata": {},
|
| 307 |
+
"source": [
|
| 308 |
+
"## SSE ๋ฐฉ์๊ณผ StdIO ๋ฐฉ์ ํผํฉ ์ฌ์ฉ\n",
|
| 309 |
+
"\n",
|
| 310 |
+
"- ํ์ผ: `mcp_server_rag.py` ๋ StdIO ๋ฐฉ์์ผ๋ก ํต์ \n",
|
| 311 |
+
"- `langchain-dev-docs` ๋ SSE ๋ฐฉ์์ผ๋ก ํต์ \n",
|
| 312 |
+
"\n",
|
| 313 |
+
"SSE ๋ฐฉ์๊ณผ StdIO ๋ฐฉ์์ ํผํฉํ์ฌ ์ฌ์ฉํฉ๋๋ค."
|
| 314 |
+
]
|
| 315 |
+
},
|
| 316 |
+
{
|
| 317 |
+
"cell_type": "code",
|
| 318 |
+
"execution_count": null,
|
| 319 |
+
"metadata": {},
|
| 320 |
+
"outputs": [],
|
| 321 |
+
"source": [
|
| 322 |
+
"from langchain_mcp_adapters.client import MultiServerMCPClient\n",
|
| 323 |
+
"from langgraph.prebuilt import create_react_agent\n",
|
| 324 |
+
"from langchain_anthropic import ChatAnthropic\n",
|
| 325 |
+
"\n",
|
| 326 |
+
"# Anthropic์ Claude ๋ชจ๋ธ ์ด๊ธฐํ\n",
|
| 327 |
+
"model = ChatAnthropic(\n",
|
| 328 |
+
" model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n",
|
| 329 |
+
")\n",
|
| 330 |
+
"\n",
|
| 331 |
+
"# 1. ๋ค์ค ์๋ฒ MCP ํด๋ผ์ด์ธํธ ์์ฑ\n",
|
| 332 |
+
"client = MultiServerMCPClient(\n",
|
| 333 |
+
" {\n",
|
| 334 |
+
" \"document-retriever\": {\n",
|
| 335 |
+
" \"command\": \"./.venv/bin/python\",\n",
|
| 336 |
+
" # mcp_server_rag.py ํ์ผ์ ์ ๋ ๊ฒฝ๋ก๋ก ์
๋ฐ์ดํธํด์ผ ํฉ๋๋ค\n",
|
| 337 |
+
" \"args\": [\"./mcp_server_rag.py\"],\n",
|
| 338 |
+
" # stdio ๋ฐฉ์์ผ๋ก ํต์ (ํ์ค ์
์ถ๋ ฅ ์ฌ์ฉ)\n",
|
| 339 |
+
" \"transport\": \"stdio\",\n",
|
| 340 |
+
" },\n",
|
| 341 |
+
" \"langchain-dev-docs\": {\n",
|
| 342 |
+
" # SSE ์๋ฒ๊ฐ ์คํ ์ค์ธ์ง ํ์ธํ์ธ์\n",
|
| 343 |
+
" \"url\": \"https://teddynote.io/mcp/langchain/sse\",\n",
|
| 344 |
+
" # SSE(Server-Sent Events) ๋ฐฉ์์ผ๋ก ํต์ \n",
|
| 345 |
+
" \"transport\": \"sse\",\n",
|
| 346 |
+
" },\n",
|
| 347 |
+
" }\n",
|
| 348 |
+
")\n",
|
| 349 |
+
"\n",
|
| 350 |
+
"\n",
|
| 351 |
+
"# 2. ๋น๋๊ธฐ ์ปจํ
์คํธ ๋งค๋์ ๋ฅผ ํตํ ๋ช
์์ ์ฐ๊ฒฐ ์ด๊ธฐํ\n",
|
| 352 |
+
"await client.__aenter__()"
|
| 353 |
+
]
|
| 354 |
+
},
|
| 355 |
+
{
|
| 356 |
+
"cell_type": "markdown",
|
| 357 |
+
"metadata": {},
|
| 358 |
+
"source": [
|
| 359 |
+
"langgraph ์ `create_react_agent` ๋ฅผ ์ฌ์ฉํ์ฌ ์์ด์ ํธ๋ฅผ ์์ฑํฉ๋๋ค."
|
| 360 |
+
]
|
| 361 |
+
},
|
| 362 |
+
{
|
| 363 |
+
"cell_type": "code",
|
| 364 |
+
"execution_count": 10,
|
| 365 |
+
"metadata": {},
|
| 366 |
+
"outputs": [],
|
| 367 |
+
"source": [
|
| 368 |
+
"from langgraph.checkpoint.memory import MemorySaver\n",
|
| 369 |
+
"from langchain_core.runnables import RunnableConfig\n",
|
| 370 |
+
"\n",
|
| 371 |
+
"prompt = (\n",
|
| 372 |
+
" \"You are a smart agent. \"\n",
|
| 373 |
+
" \"Use `retriever` tool to search on AI related documents and answer questions.\"\n",
|
| 374 |
+
" \"Use `langchain-dev-docs` tool to search on langchain / langgraph related documents and answer questions.\"\n",
|
| 375 |
+
" \"Answer in Korean.\"\n",
|
| 376 |
+
")\n",
|
| 377 |
+
"agent = create_react_agent(\n",
|
| 378 |
+
" model, client.get_tools(), prompt=prompt, checkpointer=MemorySaver()\n",
|
| 379 |
+
")"
|
| 380 |
+
]
|
| 381 |
+
},
|
| 382 |
+
{
|
| 383 |
+
"cell_type": "markdown",
|
| 384 |
+
"metadata": {},
|
| 385 |
+
"source": [
|
| 386 |
+
"๊ตฌ์ถํด ๋์ `mcp_server_rag.py` ์์ ์ ์ํ `retriever` ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ฌ ๊ฒ์์ ์ํํฉ๋๋ค."
|
| 387 |
+
]
|
| 388 |
+
},
|
| 389 |
+
{
|
| 390 |
+
"cell_type": "code",
|
| 391 |
+
"execution_count": null,
|
| 392 |
+
"metadata": {},
|
| 393 |
+
"outputs": [],
|
| 394 |
+
"source": [
|
| 395 |
+
"config = RunnableConfig(recursion_limit=30, thread_id=1)\n",
|
| 396 |
+
"await astream_graph(\n",
|
| 397 |
+
" agent,\n",
|
| 398 |
+
" {\n",
|
| 399 |
+
" \"messages\": \"`retriever` ๋๊ตฌ๋ฅผ ์ฌ์ฉํด์ ์ผ์ฑ์ ์๊ฐ ๊ฐ๋ฐํ ์์ฑํ AI ์ด๋ฆ์ ๊ฒ์ํด์ค\"\n",
|
| 400 |
+
" },\n",
|
| 401 |
+
" config=config,\n",
|
| 402 |
+
")"
|
| 403 |
+
]
|
| 404 |
+
},
|
| 405 |
+
{
|
| 406 |
+
"cell_type": "markdown",
|
| 407 |
+
"metadata": {},
|
| 408 |
+
"source": [
|
| 409 |
+
"์ด๋ฒ์๋ `langchain-dev-docs` ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ฌ ๊ฒ์์ ์ํํฉ๋๋ค."
|
| 410 |
+
]
|
| 411 |
+
},
|
| 412 |
+
{
|
| 413 |
+
"cell_type": "code",
|
| 414 |
+
"execution_count": null,
|
| 415 |
+
"metadata": {},
|
| 416 |
+
"outputs": [],
|
| 417 |
+
"source": [
|
| 418 |
+
"config = RunnableConfig(recursion_limit=30, thread_id=1)\n",
|
| 419 |
+
"await astream_graph(\n",
|
| 420 |
+
" agent,\n",
|
| 421 |
+
" {\"messages\": \"langgraph-dev-docs ์ฐธ๊ณ ํด์ self-rag ์ ์ ์์ ๋ํด์ ์๋ ค์ค\"},\n",
|
| 422 |
+
" config=config,\n",
|
| 423 |
+
")"
|
| 424 |
+
]
|
| 425 |
+
},
|
| 426 |
+
{
|
| 427 |
+
"cell_type": "markdown",
|
| 428 |
+
"metadata": {},
|
| 429 |
+
"source": [
|
| 430 |
+
"`MemorySaver` ๋ฅผ ์ฌ์ฉํ์ฌ ๋จ๊ธฐ ๊ธฐ์ต์ ์ ์งํฉ๋๋ค. ๋ฐ๋ผ์, multi-turn ๋ํ๋ ๊ฐ๋ฅํฉ๋๋ค."
|
| 431 |
+
]
|
| 432 |
+
},
|
| 433 |
+
{
|
| 434 |
+
"cell_type": "code",
|
| 435 |
+
"execution_count": null,
|
| 436 |
+
"metadata": {},
|
| 437 |
+
"outputs": [],
|
| 438 |
+
"source": [
|
| 439 |
+
"await astream_graph(\n",
|
| 440 |
+
" agent, {\"messages\": \"์ด์ ์ ๋ด์ฉ์ bullet point ๋ก ์์ฝํด์ค\"}, config=config\n",
|
| 441 |
+
")"
|
| 442 |
+
]
|
| 443 |
+
},
|
| 444 |
+
{
|
| 445 |
+
"cell_type": "markdown",
|
| 446 |
+
"metadata": {},
|
| 447 |
+
"source": [
|
| 448 |
+
"## LangChain ์ ํตํฉ๋ ๋๊ตฌ + MCP ๋๊ตฌ\n",
|
| 449 |
+
"\n",
|
| 450 |
+
"์ฌ๊ธฐ์๋ LangChain ์ ํตํฉ๋ ๋๊ตฌ๋ฅผ ๊ธฐ์กด์ MCP ๋ก๋ง ์ด๋ฃจ์ด์ง ๋๊ตฌ์ ํจ๊ป ์ฌ์ฉ์ด ๊ฐ๋ฅํ์ง ํ
์คํธ ํฉ๋๋ค."
|
| 451 |
+
]
|
| 452 |
+
},
|
| 453 |
+
{
|
| 454 |
+
"cell_type": "code",
|
| 455 |
+
"execution_count": 14,
|
| 456 |
+
"metadata": {},
|
| 457 |
+
"outputs": [],
|
| 458 |
+
"source": [
|
| 459 |
+
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
|
| 460 |
+
"\n",
|
| 461 |
+
"# Tavily ๊ฒ์ ๋๊ตฌ๋ฅผ ์ด๊ธฐํ ํฉ๋๋ค. (news ํ์
, ์ต๊ทผ 3์ผ ๋ด ๋ด์ค)\n",
|
| 462 |
+
"tavily = TavilySearchResults(max_results=3, topic=\"news\", days=3)\n",
|
| 463 |
+
"\n",
|
| 464 |
+
"# ๊ธฐ์กด์ MCP ๋๊ตฌ์ ํจ๊ป ์ฌ์ฉํฉ๋๋ค.\n",
|
| 465 |
+
"tools = client.get_tools() + [tavily]"
|
| 466 |
+
]
|
| 467 |
+
},
|
| 468 |
+
{
|
| 469 |
+
"cell_type": "markdown",
|
| 470 |
+
"metadata": {},
|
| 471 |
+
"source": [
|
| 472 |
+
"langgraph ์ `create_react_agent` ๋ฅผ ์ฌ์ฉํ์ฌ ์์ด์ ํธ๋ฅผ ์์ฑํฉ๋๋ค."
|
| 473 |
+
]
|
| 474 |
+
},
|
| 475 |
+
{
|
| 476 |
+
"cell_type": "code",
|
| 477 |
+
"execution_count": 15,
|
| 478 |
+
"metadata": {},
|
| 479 |
+
"outputs": [],
|
| 480 |
+
"source": [
|
| 481 |
+
"from langgraph.checkpoint.memory import MemorySaver\n",
|
| 482 |
+
"from langchain_core.runnables import RunnableConfig\n",
|
| 483 |
+
"\n",
|
| 484 |
+
"# ์ฌ๊ท ์ ํ ๋ฐ ์ค๋ ๋ ์์ด๋ ์ค์ \n",
|
| 485 |
+
"config = RunnableConfig(recursion_limit=30, thread_id=2)\n",
|
| 486 |
+
"\n",
|
| 487 |
+
"# ํ๋กฌํํธ ์ค์ \n",
|
| 488 |
+
"prompt = \"You are a smart agent with various tools. Answer questions in Korean.\"\n",
|
| 489 |
+
"\n",
|
| 490 |
+
"# ์์ด์ ํธ ์์ฑ\n",
|
| 491 |
+
"agent = create_react_agent(model, tools, prompt=prompt, checkpointer=MemorySaver())"
|
| 492 |
+
]
|
| 493 |
+
},
|
| 494 |
+
{
|
| 495 |
+
"cell_type": "markdown",
|
| 496 |
+
"metadata": {},
|
| 497 |
+
"source": [
|
| 498 |
+
"์๋กญ๊ฒ ์ถ๊ฐํ `tavily` ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ฌ ๊ฒ์์ ์ํํฉ๋๋ค."
|
| 499 |
+
]
|
| 500 |
+
},
|
| 501 |
+
{
|
| 502 |
+
"cell_type": "code",
|
| 503 |
+
"execution_count": null,
|
| 504 |
+
"metadata": {},
|
| 505 |
+
"outputs": [],
|
| 506 |
+
"source": [
|
| 507 |
+
"await astream_graph(agent, {\"messages\": \"์ค๋ ๋ด์ค ์ฐพ์์ค\"}, config=config)"
|
| 508 |
+
]
|
| 509 |
+
},
|
| 510 |
+
{
|
| 511 |
+
"cell_type": "markdown",
|
| 512 |
+
"metadata": {},
|
| 513 |
+
"source": [
|
| 514 |
+
"`retriever` ๋๊ตฌ๊ฐ ์ํํ๊ฒ ์๋ํ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค."
|
| 515 |
+
]
|
| 516 |
+
},
|
| 517 |
+
{
|
| 518 |
+
"cell_type": "code",
|
| 519 |
+
"execution_count": null,
|
| 520 |
+
"metadata": {},
|
| 521 |
+
"outputs": [],
|
| 522 |
+
"source": [
|
| 523 |
+
"await astream_graph(\n",
|
| 524 |
+
" agent,\n",
|
| 525 |
+
" {\n",
|
| 526 |
+
" \"messages\": \"`retriever` ๋๊ตฌ๋ฅผ ์ฌ์ฉํด์ ์ผ์ฑ์ ์๊ฐ ๊ฐ๋ฐํ ์์ฑํ AI ์ด๋ฆ์ ๊ฒ์ํด์ค\"\n",
|
| 527 |
+
" },\n",
|
| 528 |
+
" config=config,\n",
|
| 529 |
+
")"
|
| 530 |
+
]
|
| 531 |
+
},
|
| 532 |
+
{
|
| 533 |
+
"cell_type": "markdown",
|
| 534 |
+
"metadata": {},
|
| 535 |
+
"source": [
|
| 536 |
+
"## Smithery ์์ ์ ๊ณตํ๋ MCP ์๋ฒ\n",
|
| 537 |
+
"\n",
|
| 538 |
+
"- ๋งํฌ: https://smithery.ai/"
|
| 539 |
+
]
|
| 540 |
+
},
|
| 541 |
+
{
|
| 542 |
+
"cell_type": "markdown",
|
| 543 |
+
"metadata": {},
|
| 544 |
+
"source": [
|
| 545 |
+
"์ฌ์ฉํ ๋๊ตฌ ๋ชฉ๋ก์ ์๋์ ๊ฐ์ต๋๋ค.\n",
|
| 546 |
+
"\n",
|
| 547 |
+
"- Sequential Thinking: https://smithery.ai/server/@smithery-ai/server-sequential-thinking\n",
|
| 548 |
+
" - ๊ตฌ์กฐํ๋ ์ฌ๊ณ ํ๋ก์ธ์ค๋ฅผ ํตํด ์ญ๋์ ์ด๊ณ ์ฑ์ฐฐ์ ์ธ ๋ฌธ์ ํด๊ฒฐ์ ์ํ ๋๊ตฌ๋ฅผ ์ ๊ณตํ๋ MCP ์๋ฒ\n",
|
| 549 |
+
"- Desktop Commander: https://smithery.ai/server/@wonderwhy-er/desktop-commander\n",
|
| 550 |
+
" - ๋ค์ํ ํธ์ง ๊ธฐ๋ฅ์ผ๋ก ํฐ๋ฏธ๋ ๋ช
๋ น์ ์คํํ๊ณ ํ์ผ์ ๊ด๋ฆฌํ์ธ์. ์ฝ๋ฉ, ์
ธ ๋ฐ ํฐ๋ฏธ๋, ์์
์๋ํ\n",
|
| 551 |
+
"\n",
|
| 552 |
+
"**์ฐธ๊ณ **\n",
|
| 553 |
+
"\n",
|
| 554 |
+
"- smithery ์์ ์ ๊ณตํ๋ ๋๊ตฌ๋ฅผ JSON ํ์์ผ๋ก ๊ฐ์ ธ์ฌ๋, ์๋์ ์์์ฒ๋ผ `\"transport\": \"stdio\"` ๋ก ๊ผญ ์ค์ ํด์ผ ํฉ๋๋ค."
|
| 555 |
+
]
|
| 556 |
+
},
|
| 557 |
+
{
|
| 558 |
+
"cell_type": "code",
|
| 559 |
+
"execution_count": null,
|
| 560 |
+
"metadata": {},
|
| 561 |
+
"outputs": [],
|
| 562 |
+
"source": [
|
| 563 |
+
"from langchain_mcp_adapters.client import MultiServerMCPClient\n",
|
| 564 |
+
"from langgraph.prebuilt import create_react_agent\n",
|
| 565 |
+
"from langchain_anthropic import ChatAnthropic\n",
|
| 566 |
+
"\n",
|
| 567 |
+
"# LLM ๋ชจ๋ธ ์ด๊ธฐํ\n",
|
| 568 |
+
"model = ChatAnthropic(model=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000)\n",
|
| 569 |
+
"\n",
|
| 570 |
+
"# 1. ํด๋ผ์ด์ธํธ ์์ฑ\n",
|
| 571 |
+
"client = MultiServerMCPClient(\n",
|
| 572 |
+
" {\n",
|
| 573 |
+
" \"server-sequential-thinking\": {\n",
|
| 574 |
+
" \"command\": \"npx\",\n",
|
| 575 |
+
" \"args\": [\n",
|
| 576 |
+
" \"-y\",\n",
|
| 577 |
+
" \"@smithery/cli@latest\",\n",
|
| 578 |
+
" \"run\",\n",
|
| 579 |
+
" \"@smithery-ai/server-sequential-thinking\",\n",
|
| 580 |
+
" \"--key\",\n",
|
| 581 |
+
" \"89a4780a-53b7-4b7b-92e9-a29815f2669b\",\n",
|
| 582 |
+
" ],\n",
|
| 583 |
+
" \"transport\": \"stdio\", # stdio ๋ฐฉ์์ผ๋ก ํต์ ์ ์ถ๊ฐํฉ๋๋ค.\n",
|
| 584 |
+
" },\n",
|
| 585 |
+
" \"desktop-commander\": {\n",
|
| 586 |
+
" \"command\": \"npx\",\n",
|
| 587 |
+
" \"args\": [\n",
|
| 588 |
+
" \"-y\",\n",
|
| 589 |
+
" \"@smithery/cli@latest\",\n",
|
| 590 |
+
" \"run\",\n",
|
| 591 |
+
" \"@wonderwhy-er/desktop-commander\",\n",
|
| 592 |
+
" \"--key\",\n",
|
| 593 |
+
" \"89a4780a-53b7-4b7b-92e9-a29815f2669b\",\n",
|
| 594 |
+
" ],\n",
|
| 595 |
+
" \"transport\": \"stdio\", # stdio ๋ฐฉ์์ผ๋ก ํต์ ์ ์ถ๊ฐํฉ๋๋ค.\n",
|
| 596 |
+
" },\n",
|
| 597 |
+
" \"document-retriever\": {\n",
|
| 598 |
+
" \"command\": \"./.venv/bin/python\",\n",
|
| 599 |
+
" # mcp_server_rag.py ํ์ผ์ ์ ๋ ๊ฒฝ๋ก๋ก ์
๋ฐ์ดํธํด์ผ ํฉ๋๋ค\n",
|
| 600 |
+
" \"args\": [\"./mcp_server_rag.py\"],\n",
|
| 601 |
+
" # stdio ๋ฐฉ์์ผ๋ก ํต์ (ํ์ค ์
์ถ๋ ฅ ์ฌ์ฉ)\n",
|
| 602 |
+
" \"transport\": \"stdio\",\n",
|
| 603 |
+
" },\n",
|
| 604 |
+
" }\n",
|
| 605 |
+
")\n",
|
| 606 |
+
"\n",
|
| 607 |
+
"\n",
|
| 608 |
+
"# 2. ๋ช
์์ ์ผ๋ก ์ฐ๊ฒฐ ์ด๊ธฐํ\n",
|
| 609 |
+
"await client.__aenter__()"
|
| 610 |
+
]
|
| 611 |
+
},
|
| 612 |
+
{
|
| 613 |
+
"cell_type": "markdown",
|
| 614 |
+
"metadata": {},
|
| 615 |
+
"source": [
|
| 616 |
+
"langgraph ์ `create_react_agent` ๋ฅผ ์ฌ์ฉํ์ฌ ์์ด์ ํธ๋ฅผ ์์ฑํฉ๋๋ค."
|
| 617 |
+
]
|
| 618 |
+
},
|
| 619 |
+
{
|
| 620 |
+
"cell_type": "code",
|
| 621 |
+
"execution_count": 19,
|
| 622 |
+
"metadata": {},
|
| 623 |
+
"outputs": [],
|
| 624 |
+
"source": [
|
| 625 |
+
"from langgraph.checkpoint.memory import MemorySaver\n",
|
| 626 |
+
"from langchain_core.runnables import RunnableConfig\n",
|
| 627 |
+
"\n",
|
| 628 |
+
"config = RunnableConfig(recursion_limit=30, thread_id=3)\n",
|
| 629 |
+
"agent = create_react_agent(model, client.get_tools(), checkpointer=MemorySaver())"
|
| 630 |
+
]
|
| 631 |
+
},
|
| 632 |
+
{
|
| 633 |
+
"cell_type": "markdown",
|
| 634 |
+
"metadata": {},
|
| 635 |
+
"source": [
|
| 636 |
+
"`Desktop Commander` ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ฌ ํฐ๋ฏธ๋ ๋ช
๋ น์ ์คํํฉ๋๋ค."
|
| 637 |
+
]
|
| 638 |
+
},
|
| 639 |
+
{
|
| 640 |
+
"cell_type": "code",
|
| 641 |
+
"execution_count": null,
|
| 642 |
+
"metadata": {},
|
| 643 |
+
"outputs": [],
|
| 644 |
+
"source": [
|
| 645 |
+
"await astream_graph(\n",
|
| 646 |
+
" agent,\n",
|
| 647 |
+
" {\n",
|
| 648 |
+
" \"messages\": \"ํ์ฌ ๊ฒฝ๋ก๋ฅผ ํฌํจํ ํ์ ํด๋ ๊ตฌ์กฐ๋ฅผ tree ๋ก ๊ทธ๋ ค์ค. ๋จ, .venv ํด๋๋ ์ ์ธํ๊ณ ์ถ๋ ฅํด์ค.\"\n",
|
| 649 |
+
" },\n",
|
| 650 |
+
" config=config,\n",
|
| 651 |
+
")"
|
| 652 |
+
]
|
| 653 |
+
},
|
| 654 |
+
{
|
| 655 |
+
"cell_type": "markdown",
|
| 656 |
+
"metadata": {},
|
| 657 |
+
"source": [
|
| 658 |
+
"์ด๋ฒ์๋ `Sequential Thinking` ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ฌ ๋น๊ต์ ๋ณต์กํ ์์
์ ์ํํ ์ ์๋์ง ํ์ธํฉ๋๋ค."
|
| 659 |
+
]
|
| 660 |
+
},
|
| 661 |
+
{
|
| 662 |
+
"cell_type": "code",
|
| 663 |
+
"execution_count": null,
|
| 664 |
+
"metadata": {},
|
| 665 |
+
"outputs": [],
|
| 666 |
+
"source": [
|
| 667 |
+
"await astream_graph(\n",
|
| 668 |
+
" agent,\n",
|
| 669 |
+
" {\n",
|
| 670 |
+
" \"messages\": (\n",
|
| 671 |
+
" \"`retriever` ๋๊ตฌ๋ฅผ ์ฌ์ฉํด์ ์ผ์ฑ์ ์๊ฐ ๊ฐ๋ฐํ ์์ฑํ AI ๊ด๋ จ ๋ด์ฉ์ ๊ฒ์ํ๊ณ \"\n",
|
| 672 |
+
" \"`Sequential Thinking` ๋๊ตฌ๋ฅผ ์ฌ์ฉํด์ ๋ณด๊ณ ์๋ฅผ ์์ฑํด์ค.\"\n",
|
| 673 |
+
" )\n",
|
| 674 |
+
" },\n",
|
| 675 |
+
" config=config,\n",
|
| 676 |
+
")"
|
| 677 |
+
]
|
| 678 |
+
}
|
| 679 |
+
],
|
| 680 |
+
"metadata": {
|
| 681 |
+
"kernelspec": {
|
| 682 |
+
"display_name": ".venv",
|
| 683 |
+
"language": "python",
|
| 684 |
+
"name": "python3"
|
| 685 |
+
},
|
| 686 |
+
"language_info": {
|
| 687 |
+
"codemirror_mode": {
|
| 688 |
+
"name": "ipython",
|
| 689 |
+
"version": 3
|
| 690 |
+
},
|
| 691 |
+
"file_extension": ".py",
|
| 692 |
+
"mimetype": "text/x-python",
|
| 693 |
+
"name": "python",
|
| 694 |
+
"nbconvert_exporter": "python",
|
| 695 |
+
"pygments_lexer": "ipython3",
|
| 696 |
+
"version": "3.12.8"
|
| 697 |
+
}
|
| 698 |
+
},
|
| 699 |
+
"nbformat": 4,
|
| 700 |
+
"nbformat_minor": 2
|
| 701 |
+
}
|
README.md
CHANGED
|
@@ -1,13 +1,224 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# LangGraph Agents + MCP
|
| 2 |
+
|
| 3 |
+
[](README.md) [](README_KOR.md)
|
| 4 |
+
|
| 5 |
+
[](https://github.com/teddylee777/langgraph-mcp-agents)
|
| 6 |
+
[](https://opensource.org/licenses/MIT)
|
| 7 |
+
[](https://www.python.org/)
|
| 8 |
+
[](https://github.com/teddylee777/langgraph-mcp-agents)
|
| 9 |
+
|
| 10 |
+

|
| 11 |
+
|
| 12 |
+
## Project Overview
|
| 13 |
+
|
| 14 |
+

|
| 15 |
+
|
| 16 |
+
`LangChain-MCP-Adapters` is a toolkit provided by **LangChain AI** that enables AI agents to interact with external tools and data sources through the Model Context Protocol (MCP). This project provides a user-friendly interface for deploying ReAct agents that can access various data sources and APIs through MCP tools.
|
| 17 |
+
|
| 18 |
+
### Features
|
| 19 |
+
|
| 20 |
+
- **Streamlit Interface**: A user-friendly web interface for interacting with LangGraph `ReAct Agent` with MCP tools
|
| 21 |
+
- **Tool Management**: Add, remove, and configure MCP tools through the UI (Smithery JSON format supported). This is done dynamically without restarting the application
|
| 22 |
+
- **Streaming Responses**: View agent responses and tool calls in real-time
|
| 23 |
+
- **Conversation History**: Track and manage conversations with the agent
|
| 24 |
+
|
| 25 |
+
## MCP Architecture
|
| 26 |
+
|
| 27 |
+
The Model Context Protocol (MCP) consists of three main components:
|
| 28 |
+
|
| 29 |
+
1. **MCP Host**: Programs seeking to access data through MCP, such as Claude Desktop, IDEs, or LangChain/LangGraph.
|
| 30 |
+
|
| 31 |
+
2. **MCP Client**: A protocol client that maintains a 1:1 connection with the server, acting as an intermediary between the host and server.
|
| 32 |
+
|
| 33 |
+
3. **MCP Server**: A lightweight program that exposes specific functionalities through a standardized model context protocol, serving as the primary data source.
|
| 34 |
+
|
| 35 |
+
## Quick Start with Docker
|
| 36 |
+
|
| 37 |
+
You can easily run this project using Docker without setting up a local Python environment.
|
| 38 |
+
|
| 39 |
+
### Requirements (Docker Desktop)
|
| 40 |
+
|
| 41 |
+
Install Docker Desktop from the link below:
|
| 42 |
+
|
| 43 |
+
- [Install Docker Desktop](https://www.docker.com/products/docker-desktop/)
|
| 44 |
+
|
| 45 |
+
### Run with Docker Compose
|
| 46 |
+
|
| 47 |
+
1. Navigate to the `dockers` directory
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
cd dockers
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
2. Create a `.env` file with your API keys in the project root directory.
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
cp .env.example .env
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
Enter your obtained API keys in the `.env` file.
|
| 60 |
+
|
| 61 |
+
(Note) Not all API keys are required. Only enter the ones you need.
|
| 62 |
+
- `ANTHROPIC_API_KEY`: If you enter an Anthropic API key, you can use "claude-3-7-sonnet-latest", "claude-3-5-sonnet-latest", "claude-3-haiku-latest" models.
|
| 63 |
+
- `OPENAI_API_KEY`: If you enter an OpenAI API key, you can use "gpt-4o", "gpt-4o-mini" models.
|
| 64 |
+
- `LANGSMITH_API_KEY`: If you enter a LangSmith API key, you can use LangSmith tracing.
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
ANTHROPIC_API_KEY=your_anthropic_api_key
|
| 68 |
+
OPENAI_API_KEY=your_openai_api_key
|
| 69 |
+
LANGSMITH_API_KEY=your_langsmith_api_key
|
| 70 |
+
LANGSMITH_TRACING=true
|
| 71 |
+
LANGSMITH_ENDPOINT=https://api.smith.langchain.com
|
| 72 |
+
LANGSMITH_PROJECT=LangGraph-MCP-Agents
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
When using the login feature, set `USE_LOGIN` to `true` and enter `USER_ID` and `USER_PASSWORD`.
|
| 76 |
+
|
| 77 |
+
```bash
|
| 78 |
+
USE_LOGIN=true
|
| 79 |
+
USER_ID=admin
|
| 80 |
+
USER_PASSWORD=admin123
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
If you don't want to use the login feature, set `USE_LOGIN` to `false`.
|
| 84 |
+
|
| 85 |
+
```bash
|
| 86 |
+
USE_LOGIN=false
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
3. Select the Docker Compose file that matches your system architecture.
|
| 90 |
+
|
| 91 |
+
**AMD64/x86_64 Architecture (Intel/AMD Processors)**
|
| 92 |
+
|
| 93 |
+
```bash
|
| 94 |
+
# Run container
|
| 95 |
+
docker compose -f docker-compose.yaml up -d
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
**ARM64 Architecture (Apple Silicon M1/M2/M3/M4)**
|
| 99 |
+
|
| 100 |
+
```bash
|
| 101 |
+
# Run container
|
| 102 |
+
docker compose -f docker-compose-mac.yaml up -d
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
4. Access the application in your browser at http://localhost:8585
|
| 106 |
+
|
| 107 |
+
(Note)
|
| 108 |
+
- If you need to modify ports or other settings, edit the docker-compose.yaml file before building.
|
| 109 |
+
|
| 110 |
+
## Install Directly from Source Code
|
| 111 |
+
|
| 112 |
+
1. Clone this repository
|
| 113 |
+
|
| 114 |
+
```bash
|
| 115 |
+
git clone https://github.com/teddynote-lab/langgraph-mcp-agents.git
|
| 116 |
+
cd langgraph-mcp-agents
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
2. Create a virtual environment and install dependencies using uv
|
| 120 |
+
|
| 121 |
+
```bash
|
| 122 |
+
uv venv
|
| 123 |
+
uv pip install -r requirements.txt
|
| 124 |
+
source .venv/bin/activate # For Windows: .venv\Scripts\activate
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
3. Create a `.env` file with your API keys (copy from `.env.example`)
|
| 128 |
+
|
| 129 |
+
```bash
|
| 130 |
+
cp .env.example .env
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
Enter your obtained API keys in the `.env` file.
|
| 134 |
+
|
| 135 |
+
(Note) Not all API keys are required. Only enter the ones you need.
|
| 136 |
+
- `ANTHROPIC_API_KEY`: If you enter an Anthropic API key, you can use "claude-3-7-sonnet-latest", "claude-3-5-sonnet-latest", "claude-3-haiku-latest" models.
|
| 137 |
+
- `OPENAI_API_KEY`: If you enter an OpenAI API key, you can use "gpt-4o", "gpt-4o-mini" models.
|
| 138 |
+
- `LANGSMITH_API_KEY`: If you enter a LangSmith API key, you can use LangSmith tracing.
|
| 139 |
+
```bash
|
| 140 |
+
ANTHROPIC_API_KEY=your_anthropic_api_key
|
| 141 |
+
OPENAI_API_KEY=your_openai_api_key
|
| 142 |
+
LANGSMITH_API_KEY=your_langsmith_api_key
|
| 143 |
+
LANGSMITH_TRACING=true
|
| 144 |
+
LANGSMITH_ENDPOINT=https://api.smith.langchain.com
|
| 145 |
+
LANGSMITH_PROJECT=LangGraph-MCP-Agents
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
4. (New) Use the login/logout feature
|
| 149 |
+
|
| 150 |
+
When using the login feature, set `USE_LOGIN` to `true` and enter `USER_ID` and `USER_PASSWORD`.
|
| 151 |
+
|
| 152 |
+
```bash
|
| 153 |
+
USE_LOGIN=true
|
| 154 |
+
USER_ID=admin
|
| 155 |
+
USER_PASSWORD=admin123
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
If you don't want to use the login feature, set `USE_LOGIN` to `false`.
|
| 159 |
+
|
| 160 |
+
```bash
|
| 161 |
+
USE_LOGIN=false
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
## Usage
|
| 165 |
+
|
| 166 |
+
1. Start the Streamlit application.
|
| 167 |
+
|
| 168 |
+
```bash
|
| 169 |
+
streamlit run app.py
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
2. The application will run in the browser and display the main interface.
|
| 173 |
+
|
| 174 |
+
3. Use the sidebar to add and configure MCP tools
|
| 175 |
+
|
| 176 |
+
Visit [Smithery](https://smithery.ai/) to find useful MCP servers.
|
| 177 |
+
|
| 178 |
+
First, select the tool you want to use.
|
| 179 |
+
|
| 180 |
+
Click the COPY button in the JSON configuration on the right.
|
| 181 |
+
|
| 182 |
+

|
| 183 |
+
|
| 184 |
+
Paste the copied JSON string in the `Tool JSON` section.
|
| 185 |
+
|
| 186 |
+
<img src="./assets/add-tools.png" alt="tool json" style="width: auto; height: auto;">
|
| 187 |
+
|
| 188 |
+
Click the `Add Tool` button to add it to the "Registered Tools List" section.
|
| 189 |
+
|
| 190 |
+
Finally, click the "Apply" button to apply the changes to initialize the agent with the new tools.
|
| 191 |
+
|
| 192 |
+
<img src="./assets/apply-tool-configuration.png" alt="tool json" style="width: auto; height: auto;">
|
| 193 |
+
|
| 194 |
+
4. Check the agent's status.
|
| 195 |
+
|
| 196 |
+

|
| 197 |
+
|
| 198 |
+
5. Interact with the ReAct agent that utilizes the configured MCP tools by asking questions in the chat interface.
|
| 199 |
+
|
| 200 |
+

|
| 201 |
+
|
| 202 |
+
## Hands-on Tutorial
|
| 203 |
+
|
| 204 |
+
For developers who want to learn more deeply about how MCP and LangGraph integration works, we provide a comprehensive Jupyter notebook tutorial:
|
| 205 |
+
|
| 206 |
+
- Link: [MCP-HandsOn-KOR.ipynb](./MCP-HandsOn-KOR.ipynb)
|
| 207 |
+
|
| 208 |
+
This hands-on tutorial covers:
|
| 209 |
+
|
| 210 |
+
1. **MCP Client Setup** - Learn how to configure and initialize the MultiServerMCPClient to connect to MCP servers
|
| 211 |
+
2. **Local MCP Server Integration** - Connect to locally running MCP servers via SSE and Stdio methods
|
| 212 |
+
3. **RAG Integration** - Access retriever tools using MCP for document retrieval capabilities
|
| 213 |
+
4. **Mixed Transport Methods** - Combine different transport protocols (SSE and Stdio) in a single agent
|
| 214 |
+
5. **LangChain Tools + MCP** - Integrate native LangChain tools alongside MCP tools
|
| 215 |
+
|
| 216 |
+
This tutorial provides practical examples with step-by-step explanations that help you understand how to build and integrate MCP tools into LangGraph agents.
|
| 217 |
+
|
| 218 |
+
## License
|
| 219 |
+
|
| 220 |
+
MIT License
|
| 221 |
+
|
| 222 |
+
## References
|
| 223 |
+
|
| 224 |
+
- https://github.com/langchain-ai/langchain-mcp-adapters
|
README_KOR.md
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# LangGraph ์์ด์ ํธ + MCP
|
| 2 |
+
|
| 3 |
+
[](README.md) [](README_KOR.md)
|
| 4 |
+
|
| 5 |
+
[](https://github.com/teddylee777/langgraph-mcp-agents)
|
| 6 |
+
[](https://opensource.org/licenses/MIT)
|
| 7 |
+
[](https://www.python.org/)
|
| 8 |
+
[](https://github.com/teddylee777/langgraph-mcp-agents)
|
| 9 |
+
|
| 10 |
+

|
| 11 |
+
|
| 12 |
+
## ํ๋ก์ ํธ ๊ฐ์
|
| 13 |
+
|
| 14 |
+

|
| 15 |
+
|
| 16 |
+
`LangChain-MCP-Adapters`๋ **LangChain AI**์์ ์ ๊ณตํ๋ ํดํท์ผ๋ก, AI ์์ด์ ํธ๊ฐ Model Context Protocol(MCP)์ ํตํด ์ธ๋ถ ๋๊ตฌ ๋ฐ ๋ฐ์ดํฐ ์์ค์ ์ํธ์์ฉํ ์ ์๊ฒ ํด์ค๋๋ค. ์ด ํ๋ก์ ํธ๋ MCP ๋๊ตฌ๋ฅผ ํตํด ๋ค์ํ ๋ฐ์ดํฐ ์์ค์ API์ ์ ๊ทผํ ์ ์๋ ReAct ์์ด์ ํธ๋ฅผ ๋ฐฐํฌํ๊ธฐ ์ํ ์ฌ์ฉ์ ์นํ์ ์ธ ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํฉ๋๋ค.
|
| 17 |
+
|
| 18 |
+
### ํน์ง
|
| 19 |
+
|
| 20 |
+
- **Streamlit ์ธํฐํ์ด์ค**: MCP ๋๊ตฌ๊ฐ ํฌํจ๋ LangGraph `ReAct Agent`์ ์ํธ์์ฉํ๊ธฐ ์ํ ์ฌ์ฉ์ ์นํ์ ์ธ ์น ์ธํฐํ์ด์ค
|
| 21 |
+
- **๋๊ตฌ ๊ด๋ฆฌ**: UI๋ฅผ ํตํด MCP ๋๊ตฌ๋ฅผ ์ถ๊ฐ, ์ ๊ฑฐ ๋ฐ ๊ตฌ์ฑ(Smithery JSON ํ์ ์ง์). ์ ํ๋ฆฌ์ผ์ด์
์ ์ฌ์์ํ์ง ์๊ณ ๋ ๋์ ์ผ๋ก ์ด๋ฃจ์ด์ง๋๋ค.
|
| 22 |
+
- **์คํธ๋ฆฌ๋ฐ ์๋ต**: ์์ด์ ํธ ์๋ต๊ณผ ๋๊ตฌ ํธ์ถ์ ์ค์๊ฐ์ผ๋ก ํ์ธ
|
| 23 |
+
- **๋ํ ๊ธฐ๋ก**: ์์ด์ ํธ์์ ๋ํ ์ถ์ ๋ฐ ๊ด๋ฆฌ
|
| 24 |
+
|
| 25 |
+
## MCP ์ํคํ
์ฒ
|
| 26 |
+
|
| 27 |
+
MCP(Model Context Protocol)๋ ์ธ ๊ฐ์ง ์ฃผ์ ๊ตฌ์ฑ ์์๋ก ์ด๋ฃจ์ด์ ธ ์์ต๋๋ค.
|
| 28 |
+
|
| 29 |
+
1. **MCP ํธ์คํธ**: Claude Desktop, IDE ๋๋ LangChain/LangGraph์ ๊ฐ์ด MCP๋ฅผ ํตํด ๋ฐ์ดํฐ์ ์ ๊ทผํ๊ณ ์ ํ๋ ํ๋ก๊ทธ๋จ.
|
| 30 |
+
|
| 31 |
+
2. **MCP ํด๋ผ์ด์ธํธ**: ์๋ฒ์ 1:1 ์ฐ๊ฒฐ์ ์ ์งํ๋ ํ๋กํ ์ฝ ํด๋ผ์ด์ธํธ๋ก, ํธ์คํธ์ ์๋ฒ ์ฌ์ด์ ์ค๊ฐ์ ์ญํ ์ ํฉ๋๋ค.
|
| 32 |
+
|
| 33 |
+
3. **MCP ์๋ฒ**: ํ์คํ๋ ๋ชจ๋ธ ์ปจํ
์คํธ ํ๋กํ ์ฝ์ ํตํด ํน์ ๊ธฐ๋ฅ์ ๋
ธ์ถํ๋ ๊ฒฝ๋ ํ๋ก๊ทธ๋จ์ผ๋ก, ์ฃผ์ ๋ฐ์ดํฐ ์์ค ์ญํ ์ ํฉ๋๋ค.
|
| 34 |
+
|
| 35 |
+
## Docker ๋ก ๋น ๋ฅธ ์คํ
|
| 36 |
+
|
| 37 |
+
๋ก์ปฌ Python ํ๊ฒฝ์ ์ค์ ํ์ง ์๊ณ ๋ Docker๋ฅผ ์ฌ์ฉํ์ฌ ์ด ํ๋ก์ ํธ๋ฅผ ์ฝ๊ฒ ์คํํ ์ ์์ต๋๋ค.
|
| 38 |
+
|
| 39 |
+
### ํ์ ์๊ตฌ์ฌํญ(Docker Desktop)
|
| 40 |
+
|
| 41 |
+
์๋์ ๋งํฌ์์ Docker Desktop์ ์ค์นํฉ๋๋ค.
|
| 42 |
+
|
| 43 |
+
- [Docker Desktop ์ค์น](https://www.docker.com/products/docker-desktop/)
|
| 44 |
+
|
| 45 |
+
### Docker Compose๋ก ์คํํ๊ธฐ
|
| 46 |
+
|
| 47 |
+
1. `dockers` ๋๋ ํ ๋ฆฌ๋ก ์ด๋
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
cd dockers
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
2. ํ๋ก์ ํธ ๋ฃจํธ ๋๋ ํ ๋ฆฌ์ API ํค๊ฐ ํฌํจ๋ `.env` ํ์ผ ์์ฑ.
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
cp .env.example .env
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
๋ฐ๊ธ ๋ฐ์ API ํค๋ฅผ `.env` ํ์ผ์ ์
๋ ฅํฉ๋๋ค.
|
| 60 |
+
|
| 61 |
+
(์ฐธ๊ณ ) ๋ชจ๋ API ํค๊ฐ ํ์ํ์ง ์์ต๋๋ค. ํ์ํ ๊ฒฝ์ฐ์๋ง ์
๋ ฅํ์ธ์.
|
| 62 |
+
- `ANTHROPIC_API_KEY`: Anthropic API ํค๋ฅผ ์
๋ ฅํ ๊ฒฝ์ฐ "claude-3-7-sonnet-latest", "claude-3-5-sonnet-latest", "claude-3-haiku-latest" ๋ชจ๋ธ์ ์ฌ์ฉํฉ๋๋ค.
|
| 63 |
+
- `OPENAI_API_KEY`: OpenAI API ํค๋ฅผ ์
๋ ฅํ ๊ฒฝ์ฐ "gpt-4o", "gpt-4o-mini" ๋ชจ๋ธ์ ์ฌ์ฉํฉ๋๋ค.
|
| 64 |
+
- `LANGSMITH_API_KEY`: LangSmith API ํค๋ฅผ ์
๋ ฅํ ๊ฒฝ์ฐ LangSmith tracing์ ์ฌ์ฉํฉ๋๋ค.
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
ANTHROPIC_API_KEY=your_anthropic_api_key
|
| 68 |
+
OPENAI_API_KEY=your_openai_api_key
|
| 69 |
+
LANGSMITH_API_KEY=your_langsmith_api_key
|
| 70 |
+
LANGSMITH_PROJECT=LangGraph-MCP-Agents
|
| 71 |
+
LANGSMITH_TRACING=true
|
| 72 |
+
LANGSMITH_ENDPOINT=https://api.smith.langchain.com
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
(์ ๊ท ๊ธฐ๋ฅ) ๋ก๊ทธ์ธ/๋ก๊ทธ์์ ๊ธฐ๋ฅ ์ฌ์ฉ
|
| 76 |
+
|
| 77 |
+
๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ์ฌ์ฉ์ `USE_LOGIN`์ `true`๋ก ์ค์ ํ๊ณ , `USER_ID`์ `USER_PASSWORD`๋ฅผ ์
๋ ฅํฉ๋๋ค.
|
| 78 |
+
|
| 79 |
+
```bash
|
| 80 |
+
USE_LOGIN=true
|
| 81 |
+
USER_ID=admin
|
| 82 |
+
USER_PASSWORD=admin123
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
๋ง์ฝ, ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๊ณ ์ถ์ง ์๋ค๋ฉด, `USE_LOGIN`์ `false`๋ก ์ค์ ํฉ๋๋ค.
|
| 86 |
+
|
| 87 |
+
```bash
|
| 88 |
+
USE_LOGIN=false
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
3. ์์คํ
์ํคํ
์ฒ์ ๋ง๋ Docker Compose ํ์ผ ์ ํ.
|
| 92 |
+
|
| 93 |
+
**AMD64/x86_64 ์ํคํ
์ฒ(Intel/AMD ํ๋ก์ธ์)**
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
# ์ปจํ
์ด๋ ์คํ
|
| 97 |
+
docker compose -f docker-compose-KOR.yaml up -d
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
**ARM64 ์ํคํ
์ฒ(Apple Silicon M1/M2/M3/M4)**
|
| 101 |
+
|
| 102 |
+
```bash
|
| 103 |
+
# ์ปจํ
์ด๋ ์คํ
|
| 104 |
+
docker compose -f docker-compose-KOR-mac.yaml up -d
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
4. ๋ธ๋ผ์ฐ์ ์์ http://localhost:8585 ๋ก ์ ํ๋ฆฌ์ผ์ด์
์ ์
|
| 108 |
+
|
| 109 |
+
(์ฐธ๊ณ )
|
| 110 |
+
- ํฌํธ๋ ๋ค๋ฅธ ์ค์ ์ ์์ ํด์ผ ํ๋ ๊ฒฝ์ฐ, ๋น๋ ์ ์ ํด๋น docker-compose-KOR.yaml ํ์ผ์ ํธ์งํ์ธ์.
|
| 111 |
+
|
| 112 |
+
## ์์ค์ฝ๋๋ก ๋ถํฐ ์ง์ ์ค์น
|
| 113 |
+
|
| 114 |
+
1. ์ด ์ ์ฅ์๋ฅผ ํด๋ก ํฉ๋๋ค
|
| 115 |
+
|
| 116 |
+
```bash
|
| 117 |
+
git clone https://github.com/teddynote-lab/langgraph-mcp-agents.git
|
| 118 |
+
cd langgraph-mcp-agents
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
2. ๊ฐ์ ํ๊ฒฝ์ ์์ฑํ๊ณ uv๋ฅผ ์ฌ์ฉํ์ฌ ์์กด์ฑ์ ์ค์นํฉ๋๋ค
|
| 122 |
+
|
| 123 |
+
```bash
|
| 124 |
+
uv venv
|
| 125 |
+
uv pip install -r requirements.txt
|
| 126 |
+
source .venv/bin/activate # Windows์ ๊ฒฝ์ฐ: .venv\Scripts\activate
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
3. API ํค๊ฐ ํฌํจ๋ `.env` ๏ฟฝ๏ฟฝ์ผ์ ์์ฑํฉ๋๋ค(`.env.example` ์์ ๋ณต์ฌ)
|
| 130 |
+
|
| 131 |
+
```bash
|
| 132 |
+
cp .env.example .env
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
๋ฐ๊ธ ๋ฐ์ API ํค๋ฅผ `.env` ํ์ผ์ ์
๋ ฅํฉ๋๋ค.
|
| 136 |
+
|
| 137 |
+
(์ฐธ๊ณ ) ๋ชจ๋ API ํค๊ฐ ํ์ํ์ง ์์ต๋๋ค. ํ์ํ ๊ฒฝ์ฐ์๋ง ์
๋ ฅํ์ธ์.
|
| 138 |
+
- `ANTHROPIC_API_KEY`: Anthropic API ํค๋ฅผ ์
๋ ฅํ ๊ฒฝ์ฐ "claude-3-7-sonnet-latest", "claude-3-5-sonnet-latest", "claude-3-haiku-latest" ๋ชจ๋ธ์ ์ฌ์ฉํฉ๋๋ค.
|
| 139 |
+
- `OPENAI_API_KEY`: OpenAI API ํค๋ฅผ ์
๋ ฅํ ๊ฒฝ์ฐ "gpt-4o", "gpt-4o-mini" ๋ชจ๋ธ์ ์ฌ์ฉํฉ๋๋ค.
|
| 140 |
+
- `LANGSMITH_API_KEY`: LangSmith API ํค๋ฅผ ์
๋ ฅํ ๊ฒฝ์ฐ LangSmith tracing์ ์ฌ์ฉํฉ๋๋ค.
|
| 141 |
+
|
| 142 |
+
```bash
|
| 143 |
+
ANTHROPIC_API_KEY=your_anthropic_api_key
|
| 144 |
+
OPENAI_API_KEY=your_openai_api_key(optional)
|
| 145 |
+
LANGSMITH_API_KEY=your_langsmith_api_key
|
| 146 |
+
LANGSMITH_PROJECT=LangGraph-MCP-Agents
|
| 147 |
+
LANGSMITH_TRACING=true
|
| 148 |
+
LANGSMITH_ENDPOINT=https://api.smith.langchain.com
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
4. (์ ๊ท ๊ธฐ๋ฅ) ๋ก๊ทธ์ธ/๋ก๊ทธ์์ ๊ธฐ๋ฅ ์ฌ์ฉ
|
| 152 |
+
|
| 153 |
+
๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ์ฌ์ฉ์ `USE_LOGIN`์ `true`๋ก ์ค์ ํ๊ณ , `USER_ID`์ `USER_PASSWORD`๋ฅผ ์
๋ ฅํฉ๋๋ค.
|
| 154 |
+
|
| 155 |
+
```bash
|
| 156 |
+
USE_LOGIN=true
|
| 157 |
+
USER_ID=admin
|
| 158 |
+
USER_PASSWORD=admin123
|
| 159 |
+
```
|
| 160 |
+
|
| 161 |
+
๋ง์ฝ, ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๊ณ ์ถ์ง ์๋ค๋ฉด, `USE_LOGIN`์ `false`๋ก ์ค์ ํฉ๋๋ค.
|
| 162 |
+
|
| 163 |
+
```bash
|
| 164 |
+
USE_LOGIN=false
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
## ์ฌ์ฉ๋ฒ
|
| 168 |
+
|
| 169 |
+
1. Streamlit ์ ํ๋ฆฌ์ผ์ด์
์ ์์ํฉ๋๋ค. (ํ๊ตญ์ด ๋ฒ์ ํ์ผ์ `app_KOR.py` ์
๋๋ค.)
|
| 170 |
+
|
| 171 |
+
```bash
|
| 172 |
+
streamlit run app_KOR.py
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
2. ์ ํ๋ฆฌ์ผ์ด์
์ด ๋ธ๋ผ์ฐ์ ์์ ์คํ๋์ด ๋ฉ์ธ ์ธํฐํ์ด์ค๋ฅผ ํ์ํฉ๋๋ค.
|
| 176 |
+
|
| 177 |
+
3. ์ฌ์ด๋๋ฐ๋ฅผ ์ฌ์ฉํ์ฌ MCP ๋๊ตฌ๋ฅผ ์ถ๊ฐํ๊ณ ๊ตฌ์ฑํฉ๋๋ค
|
| 178 |
+
|
| 179 |
+
์ ์ฉํ MCP ์๋ฒ๋ฅผ ์ฐพ์ผ๋ ค๋ฉด [Smithery](https://smithery.ai/)๋ฅผ ๋ฐฉ๋ฌธํ์ธ์.
|
| 180 |
+
|
| 181 |
+
๋จผ์ , ์ฌ์ฉํ๊ณ ์ ํ๋ ๋๊ตฌ๋ฅผ ์ ํํฉ๋๋ค.
|
| 182 |
+
|
| 183 |
+
์ค๋ฅธ์ชฝ์ JSON ๊ตฌ์ฑ์์ COPY ๋ฒํผ์ ๋๋ฆ
๋๋ค.
|
| 184 |
+
|
| 185 |
+

|
| 186 |
+
|
| 187 |
+
๋ณต์ฌ๋ JSON ๋ฌธ์์ด์ `Tool JSON` ์น์
์ ๋ถ์ฌ๋ฃ์ต๋๋ค.
|
| 188 |
+
|
| 189 |
+
<img src="./assets/add-tools.png" alt="tool json" style="width: auto; height: auto;">
|
| 190 |
+
|
| 191 |
+
`Add Tool` ๋ฒํผ์ ๋๋ฌ "Registered Tools List" ์น์
์ ์ถ๊ฐํฉ๋๋ค.
|
| 192 |
+
|
| 193 |
+
๋ง์ง๋ง์ผ๋ก, "Apply" ๋ฒํผ์ ๋๋ฌ ์๋ก์ด ๋๊ตฌ๋ก ์์ด์ ํธ๋ฅผ ์ด๊ธฐํํ๋๋ก ๋ณ๊ฒฝ์ฌํญ์ ์ ์ฉํฉ๋๋ค.
|
| 194 |
+
|
| 195 |
+
<img src="./assets/apply-tool-configuration.png" alt="tool json" style="width: auto; height: auto;">
|
| 196 |
+
|
| 197 |
+
4. ์์ด์ ํธ์ ์ํ๋ฅผ ํ์ธํฉ๋๋ค.
|
| 198 |
+
|
| 199 |
+

|
| 200 |
+
|
| 201 |
+
5. ์ฑํ
์ธํฐํ์ด์ค์์ ์ง๋ฌธ์ ํ์ฌ ๊ตฌ์ฑ๋ MCP ๋๊ตฌ๋ฅผ ํ์ฉํ๋ ReAct ์์ด์ ํธ์ ์ํธ์์ฉํฉ๋๋ค.
|
| 202 |
+
|
| 203 |
+

|
| 204 |
+
|
| 205 |
+
## ํธ์ฆ์จ ํํ ๋ฆฌ์ผ
|
| 206 |
+
|
| 207 |
+
๊ฐ๋ฐ์๊ฐ MCP์ LangGraph์ ํตํฉ ์๋ ๋ฐฉ์์ ๋ํด ๋ ๊น์ด ์์๋ณด๋ ค๋ฉด, ํฌ๊ด์ ์ธ Jupyter ๋
ธํธ๋ถ ํํ ๋ฆฌ์ผ์ ์ ๊ณตํฉ๋๋ค:
|
| 208 |
+
|
| 209 |
+
- ๋งํฌ: [MCP-HandsOn-KOR.ipynb](./MCP-HandsOn-KOR.ipynb)
|
| 210 |
+
|
| 211 |
+
์ด ํธ์ฆ์จ ํํ ๋ฆฌ์ผ์ ๋ค์ ๋ด์ฉ์ ๋ค๋ฃน๋๋ค.
|
| 212 |
+
|
| 213 |
+
1. **MCP ํด๋ผ์ด์ธํธ ์ค์ ** - MCP ์๋ฒ์ ์ฐ๊ฒฐํ๊ธฐ ์ํ MultiServerMCPClient ๊ตฌ์ฑ ๋ฐ ์ด๊ธฐํ ๋ฐฉ๋ฒ ํ์ต
|
| 214 |
+
2. **๋ก์ปฌ MCP ์๋ฒ ํตํฉ** - SSE ๋ฐ Stdio ๋ฉ์๋๋ฅผ ํตํด ๋ก์ปฌ์์ ์คํ ์ค์ธ MCP ์๋ฒ์ ์ฐ๊ฒฐ
|
| 215 |
+
3. **RAG ํตํฉ** - ๋ฌธ์ ๊ฒ์ ๊ธฐ๋ฅ์ ์ํด MCP๋ฅผ ์ฌ์ฉํ์ฌ ๋ฆฌํธ๋ฆฌ๋ฒ ๋๊ตฌ ์ ๊ทผ
|
| 216 |
+
4. **ํผํฉ ์ ์ก ๋ฐฉ๋ฒ** - ํ๋์ ์์ด์ ํธ์์ ๋ค์ํ ์ ์ก ํ๋กํ ์ฝ(SSE ๋ฐ Stdio) ๊ฒฐํฉ
|
| 217 |
+
5. **LangChain ๋๊ตฌ + MCP** - MCP ๋๊ตฌ์ ํจ๊ป ๋ค์ดํฐ๋ธ LangChain ๋๊ตฌ ํตํฉ
|
| 218 |
+
|
| 219 |
+
์ด ํํ ๋ฆฌ์ผ์ MCP ๋๊ตฌ๋ฅผ LangGraph ์์ด์ ํธ์ ๊ตฌ์ถํ๊ณ ํตํฉํ๋ ๋ฐฉ๋ฒ์ ์ดํดํ๋ ๋ฐ ๋์์ด ๋๋ ๋จ๊ณ๋ณ ์ค๋ช
์ด ํฌํจ๋ ์ค์ฉ์ ์ธ ์์ ๋ฅผ ์ ๊ณตํฉ๋๋ค.
|
| 220 |
+
|
| 221 |
+
## ๋ผ์ด์ ์ค
|
| 222 |
+
|
| 223 |
+
MIT License
|
| 224 |
+
|
| 225 |
+
## ํํ ๋ฆฌ์ผ ๋น๋์ค ๋ณด๊ธฐ(ํ๊ตญ์ด)
|
| 226 |
+
|
| 227 |
+
[](https://youtu.be/ISrYHGg2C2c?si=eWmKFVUS1BLtPm5U)
|
| 228 |
+
|
| 229 |
+
## ์ฐธ๊ณ ์๋ฃ
|
| 230 |
+
|
| 231 |
+
- https://github.com/langchain-ai/langchain-mcp-adapters
|
| 232 |
+
|
app.py
ADDED
|
@@ -0,0 +1,865 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import asyncio
|
| 3 |
+
import nest_asyncio
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
import platform
|
| 7 |
+
|
| 8 |
+
if platform.system() == "Windows":
|
| 9 |
+
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
| 10 |
+
|
| 11 |
+
# Apply nest_asyncio: Allow nested calls within an already running event loop
|
| 12 |
+
nest_asyncio.apply()
|
| 13 |
+
|
| 14 |
+
# Create and reuse global event loop (create once and continue using)
|
| 15 |
+
if "event_loop" not in st.session_state:
|
| 16 |
+
loop = asyncio.new_event_loop()
|
| 17 |
+
st.session_state.event_loop = loop
|
| 18 |
+
asyncio.set_event_loop(loop)
|
| 19 |
+
|
| 20 |
+
from langgraph.prebuilt import create_react_agent
|
| 21 |
+
from langchain_anthropic import ChatAnthropic
|
| 22 |
+
from langchain_openai import ChatOpenAI
|
| 23 |
+
from langchain_core.messages import HumanMessage
|
| 24 |
+
from dotenv import load_dotenv
|
| 25 |
+
from langchain_mcp_adapters.client import MultiServerMCPClient
|
| 26 |
+
from utils import astream_graph, random_uuid
|
| 27 |
+
from langchain_core.messages.ai import AIMessageChunk
|
| 28 |
+
from langchain_core.messages.tool import ToolMessage
|
| 29 |
+
from langgraph.checkpoint.memory import MemorySaver
|
| 30 |
+
from langchain_core.runnables import RunnableConfig
|
| 31 |
+
|
| 32 |
+
# Load environment variables (get API keys and settings from .env file)
|
| 33 |
+
load_dotenv(override=True)
|
| 34 |
+
|
| 35 |
+
# config.json file path setting
|
| 36 |
+
CONFIG_FILE_PATH = "config.json"
|
| 37 |
+
|
| 38 |
+
# Function to load settings from JSON file
|
| 39 |
+
def load_config_from_json():
|
| 40 |
+
"""
|
| 41 |
+
Loads settings from config.json file.
|
| 42 |
+
Creates a file with default settings if it doesn't exist.
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
dict: Loaded settings
|
| 46 |
+
"""
|
| 47 |
+
default_config = {
|
| 48 |
+
"get_current_time": {
|
| 49 |
+
"command": "python",
|
| 50 |
+
"args": ["./mcp_server_time.py"],
|
| 51 |
+
"transport": "stdio"
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
try:
|
| 56 |
+
if os.path.exists(CONFIG_FILE_PATH):
|
| 57 |
+
with open(CONFIG_FILE_PATH, "r", encoding="utf-8") as f:
|
| 58 |
+
return json.load(f)
|
| 59 |
+
else:
|
| 60 |
+
# Create file with default settings if it doesn't exist
|
| 61 |
+
save_config_to_json(default_config)
|
| 62 |
+
return default_config
|
| 63 |
+
except Exception as e:
|
| 64 |
+
st.error(f"Error loading settings file: {str(e)}")
|
| 65 |
+
return default_config
|
| 66 |
+
|
| 67 |
+
# Function to save settings to JSON file
|
| 68 |
+
def save_config_to_json(config):
|
| 69 |
+
"""
|
| 70 |
+
Saves settings to config.json file.
|
| 71 |
+
|
| 72 |
+
Args:
|
| 73 |
+
config (dict): Settings to save
|
| 74 |
+
|
| 75 |
+
Returns:
|
| 76 |
+
bool: Save success status
|
| 77 |
+
"""
|
| 78 |
+
try:
|
| 79 |
+
with open(CONFIG_FILE_PATH, "w", encoding="utf-8") as f:
|
| 80 |
+
json.dump(config, f, indent=2, ensure_ascii=False)
|
| 81 |
+
return True
|
| 82 |
+
except Exception as e:
|
| 83 |
+
st.error(f"Error saving settings file: {str(e)}")
|
| 84 |
+
return False
|
| 85 |
+
|
| 86 |
+
# Initialize login session variables
|
| 87 |
+
if "authenticated" not in st.session_state:
|
| 88 |
+
st.session_state.authenticated = False
|
| 89 |
+
|
| 90 |
+
# Check if login is required
|
| 91 |
+
use_login = os.environ.get("USE_LOGIN", "false").lower() == "true"
|
| 92 |
+
|
| 93 |
+
# Change page settings based on login status
|
| 94 |
+
if use_login and not st.session_state.authenticated:
|
| 95 |
+
# Login page uses default (narrow) layout
|
| 96 |
+
st.set_page_config(page_title="Agent with MCP Tools", page_icon="๐ง ")
|
| 97 |
+
else:
|
| 98 |
+
# Main app uses wide layout
|
| 99 |
+
st.set_page_config(page_title="Agent with MCP Tools", page_icon="๐ง ", layout="wide")
|
| 100 |
+
|
| 101 |
+
# Display login screen if login feature is enabled and not yet authenticated
|
| 102 |
+
if use_login and not st.session_state.authenticated:
|
| 103 |
+
st.title("๐ Login")
|
| 104 |
+
st.markdown("Login is required to use the system.")
|
| 105 |
+
|
| 106 |
+
# Place login form in the center of the screen with narrow width
|
| 107 |
+
with st.form("login_form"):
|
| 108 |
+
username = st.text_input("Username")
|
| 109 |
+
password = st.text_input("Password", type="password")
|
| 110 |
+
submit_button = st.form_submit_button("Login")
|
| 111 |
+
|
| 112 |
+
if submit_button:
|
| 113 |
+
expected_username = os.environ.get("USER_ID")
|
| 114 |
+
expected_password = os.environ.get("USER_PASSWORD")
|
| 115 |
+
|
| 116 |
+
if username == expected_username and password == expected_password:
|
| 117 |
+
st.session_state.authenticated = True
|
| 118 |
+
st.success("โ
Login successful! Please wait...")
|
| 119 |
+
st.rerun()
|
| 120 |
+
else:
|
| 121 |
+
st.error("โ Username or password is incorrect.")
|
| 122 |
+
|
| 123 |
+
# Don't display the main app on the login screen
|
| 124 |
+
st.stop()
|
| 125 |
+
|
| 126 |
+
# Add author information at the top of the sidebar (placed before other sidebar elements)
|
| 127 |
+
st.sidebar.markdown("### โ๏ธ Made by [TeddyNote](https://youtube.com/c/teddynote) ๐")
|
| 128 |
+
st.sidebar.markdown(
|
| 129 |
+
"### ๐ป [Project Page](https://github.com/teddynote-lab/langgraph-mcp-agents)"
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
st.sidebar.divider() # Add divider
|
| 133 |
+
|
| 134 |
+
# Existing page title and description
|
| 135 |
+
st.title("๐ฌ MCP Tool Utilization Agent")
|
| 136 |
+
st.markdown("โจ Ask questions to the ReAct agent that utilizes MCP tools.")
|
| 137 |
+
|
| 138 |
+
SYSTEM_PROMPT = """<ROLE>
|
| 139 |
+
You are a smart agent with an ability to use tools.
|
| 140 |
+
You will be given a question and you will use the tools to answer the question.
|
| 141 |
+
Pick the most relevant tool to answer the question.
|
| 142 |
+
If you are failed to answer the question, try different tools to get context.
|
| 143 |
+
Your answer should be very polite and professional.
|
| 144 |
+
</ROLE>
|
| 145 |
+
|
| 146 |
+
----
|
| 147 |
+
|
| 148 |
+
<INSTRUCTIONS>
|
| 149 |
+
Step 1: Analyze the question
|
| 150 |
+
- Analyze user's question and final goal.
|
| 151 |
+
- If the user's question is consist of multiple sub-questions, split them into smaller sub-questions.
|
| 152 |
+
|
| 153 |
+
Step 2: Pick the most relevant tool
|
| 154 |
+
- Pick the most relevant tool to answer the question.
|
| 155 |
+
- If you are failed to answer the question, try different tools to get context.
|
| 156 |
+
|
| 157 |
+
Step 3: Answer the question
|
| 158 |
+
- Answer the question in the same language as the question.
|
| 159 |
+
- Your answer should be very polite and professional.
|
| 160 |
+
|
| 161 |
+
Step 4: Provide the source of the answer(if applicable)
|
| 162 |
+
- If you've used the tool, provide the source of the answer.
|
| 163 |
+
- Valid sources are either a website(URL) or a document(PDF, etc).
|
| 164 |
+
|
| 165 |
+
Guidelines:
|
| 166 |
+
- 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).
|
| 167 |
+
- If you've used the tool, and the source is valid URL, provide the source(URL) of the answer.
|
| 168 |
+
- Skip providing the source if the source is not URL.
|
| 169 |
+
- Answer in the same language as the question.
|
| 170 |
+
- Answer should be concise and to the point.
|
| 171 |
+
- Avoid response your output with any other information than the answer and the source.
|
| 172 |
+
</INSTRUCTIONS>
|
| 173 |
+
|
| 174 |
+
----
|
| 175 |
+
|
| 176 |
+
<OUTPUT_FORMAT>
|
| 177 |
+
(concise answer to the question)
|
| 178 |
+
|
| 179 |
+
**Source**(if applicable)
|
| 180 |
+
- (source1: valid URL)
|
| 181 |
+
- (source2: valid URL)
|
| 182 |
+
- ...
|
| 183 |
+
</OUTPUT_FORMAT>
|
| 184 |
+
"""
|
| 185 |
+
|
| 186 |
+
OUTPUT_TOKEN_INFO = {
|
| 187 |
+
"claude-3-5-sonnet-latest": {"max_tokens": 8192},
|
| 188 |
+
"claude-3-5-haiku-latest": {"max_tokens": 8192},
|
| 189 |
+
"claude-3-7-sonnet-latest": {"max_tokens": 64000},
|
| 190 |
+
"gpt-4o": {"max_tokens": 16000},
|
| 191 |
+
"gpt-4o-mini": {"max_tokens": 16000},
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
# Initialize session state
|
| 195 |
+
if "session_initialized" not in st.session_state:
|
| 196 |
+
st.session_state.session_initialized = False # Session initialization flag
|
| 197 |
+
st.session_state.agent = None # Storage for ReAct agent object
|
| 198 |
+
st.session_state.history = [] # List for storing conversation history
|
| 199 |
+
st.session_state.mcp_client = None # Storage for MCP client object
|
| 200 |
+
st.session_state.timeout_seconds = (
|
| 201 |
+
120 # Response generation time limit (seconds), default 120 seconds
|
| 202 |
+
)
|
| 203 |
+
st.session_state.selected_model = (
|
| 204 |
+
"claude-3-7-sonnet-latest" # Default model selection
|
| 205 |
+
)
|
| 206 |
+
st.session_state.recursion_limit = 100 # Recursion call limit, default 100
|
| 207 |
+
|
| 208 |
+
if "thread_id" not in st.session_state:
|
| 209 |
+
st.session_state.thread_id = random_uuid()
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
# --- Function Definitions ---
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
async def cleanup_mcp_client():
|
| 216 |
+
"""
|
| 217 |
+
Safely terminates the existing MCP client.
|
| 218 |
+
|
| 219 |
+
Properly releases resources if an existing client exists.
|
| 220 |
+
"""
|
| 221 |
+
if "mcp_client" in st.session_state and st.session_state.mcp_client is not None:
|
| 222 |
+
try:
|
| 223 |
+
|
| 224 |
+
await st.session_state.mcp_client.__aexit__(None, None, None)
|
| 225 |
+
st.session_state.mcp_client = None
|
| 226 |
+
except Exception as e:
|
| 227 |
+
import traceback
|
| 228 |
+
|
| 229 |
+
# st.warning(f"Error while terminating MCP client: {str(e)}")
|
| 230 |
+
# st.warning(traceback.format_exc())
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def print_message():
|
| 234 |
+
"""
|
| 235 |
+
Displays chat history on the screen.
|
| 236 |
+
|
| 237 |
+
Distinguishes between user and assistant messages on the screen,
|
| 238 |
+
and displays tool call information within the assistant message container.
|
| 239 |
+
"""
|
| 240 |
+
i = 0
|
| 241 |
+
while i < len(st.session_state.history):
|
| 242 |
+
message = st.session_state.history[i]
|
| 243 |
+
|
| 244 |
+
if message["role"] == "user":
|
| 245 |
+
st.chat_message("user", avatar="๐งโ๐ป").markdown(message["content"])
|
| 246 |
+
i += 1
|
| 247 |
+
elif message["role"] == "assistant":
|
| 248 |
+
# Create assistant message container
|
| 249 |
+
with st.chat_message("assistant", avatar="๐ค"):
|
| 250 |
+
# Display assistant message content
|
| 251 |
+
st.markdown(message["content"])
|
| 252 |
+
|
| 253 |
+
# Check if the next message is tool call information
|
| 254 |
+
if (
|
| 255 |
+
i + 1 < len(st.session_state.history)
|
| 256 |
+
and st.session_state.history[i + 1]["role"] == "assistant_tool"
|
| 257 |
+
):
|
| 258 |
+
# Display tool call information in the same container as an expander
|
| 259 |
+
with st.expander("๐ง Tool Call Information", expanded=False):
|
| 260 |
+
st.markdown(st.session_state.history[i + 1]["content"])
|
| 261 |
+
i += 2 # Increment by 2 as we processed two messages together
|
| 262 |
+
else:
|
| 263 |
+
i += 1 # Increment by 1 as we only processed a regular message
|
| 264 |
+
else:
|
| 265 |
+
# Skip assistant_tool messages as they are handled above
|
| 266 |
+
i += 1
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def get_streaming_callback(text_placeholder, tool_placeholder):
|
| 270 |
+
"""
|
| 271 |
+
Creates a streaming callback function.
|
| 272 |
+
|
| 273 |
+
This function creates a callback function to display responses generated from the LLM in real-time.
|
| 274 |
+
It displays text responses and tool call information in separate areas.
|
| 275 |
+
|
| 276 |
+
Args:
|
| 277 |
+
text_placeholder: Streamlit component to display text responses
|
| 278 |
+
tool_placeholder: Streamlit component to display tool call information
|
| 279 |
+
|
| 280 |
+
Returns:
|
| 281 |
+
callback_func: Streaming callback function
|
| 282 |
+
accumulated_text: List to store accumulated text responses
|
| 283 |
+
accumulated_tool: List to store accumulated tool call information
|
| 284 |
+
"""
|
| 285 |
+
accumulated_text = []
|
| 286 |
+
accumulated_tool = []
|
| 287 |
+
|
| 288 |
+
def callback_func(message: dict):
|
| 289 |
+
nonlocal accumulated_text, accumulated_tool
|
| 290 |
+
message_content = message.get("content", None)
|
| 291 |
+
|
| 292 |
+
if isinstance(message_content, AIMessageChunk):
|
| 293 |
+
content = message_content.content
|
| 294 |
+
# If content is in list form (mainly occurs in Claude models)
|
| 295 |
+
if isinstance(content, list) and len(content) > 0:
|
| 296 |
+
message_chunk = content[0]
|
| 297 |
+
# Process text type
|
| 298 |
+
if message_chunk["type"] == "text":
|
| 299 |
+
accumulated_text.append(message_chunk["text"])
|
| 300 |
+
text_placeholder.markdown("".join(accumulated_text))
|
| 301 |
+
# Process tool use type
|
| 302 |
+
elif message_chunk["type"] == "tool_use":
|
| 303 |
+
if "partial_json" in message_chunk:
|
| 304 |
+
accumulated_tool.append(message_chunk["partial_json"])
|
| 305 |
+
else:
|
| 306 |
+
tool_call_chunks = message_content.tool_call_chunks
|
| 307 |
+
tool_call_chunk = tool_call_chunks[0]
|
| 308 |
+
accumulated_tool.append(
|
| 309 |
+
"\n```json\n" + str(tool_call_chunk) + "\n```\n"
|
| 310 |
+
)
|
| 311 |
+
with tool_placeholder.expander(
|
| 312 |
+
"๐ง Tool Call Information", expanded=True
|
| 313 |
+
):
|
| 314 |
+
st.markdown("".join(accumulated_tool))
|
| 315 |
+
# Process if tool_calls attribute exists (mainly occurs in OpenAI models)
|
| 316 |
+
elif (
|
| 317 |
+
hasattr(message_content, "tool_calls")
|
| 318 |
+
and message_content.tool_calls
|
| 319 |
+
and len(message_content.tool_calls[0]["name"]) > 0
|
| 320 |
+
):
|
| 321 |
+
tool_call_info = message_content.tool_calls[0]
|
| 322 |
+
accumulated_tool.append("\n```json\n" + str(tool_call_info) + "\n```\n")
|
| 323 |
+
with tool_placeholder.expander(
|
| 324 |
+
"๐ง Tool Call Information", expanded=True
|
| 325 |
+
):
|
| 326 |
+
st.markdown("".join(accumulated_tool))
|
| 327 |
+
# Process if content is a simple string
|
| 328 |
+
elif isinstance(content, str):
|
| 329 |
+
accumulated_text.append(content)
|
| 330 |
+
text_placeholder.markdown("".join(accumulated_text))
|
| 331 |
+
# Process if invalid tool call information exists
|
| 332 |
+
elif (
|
| 333 |
+
hasattr(message_content, "invalid_tool_calls")
|
| 334 |
+
and message_content.invalid_tool_calls
|
| 335 |
+
):
|
| 336 |
+
tool_call_info = message_content.invalid_tool_calls[0]
|
| 337 |
+
accumulated_tool.append("\n```json\n" + str(tool_call_info) + "\n```\n")
|
| 338 |
+
with tool_placeholder.expander(
|
| 339 |
+
"๐ง Tool Call Information (Invalid)", expanded=True
|
| 340 |
+
):
|
| 341 |
+
st.markdown("".join(accumulated_tool))
|
| 342 |
+
# Process if tool_call_chunks attribute exists
|
| 343 |
+
elif (
|
| 344 |
+
hasattr(message_content, "tool_call_chunks")
|
| 345 |
+
and message_content.tool_call_chunks
|
| 346 |
+
):
|
| 347 |
+
tool_call_chunk = message_content.tool_call_chunks[0]
|
| 348 |
+
accumulated_tool.append(
|
| 349 |
+
"\n```json\n" + str(tool_call_chunk) + "\n```\n"
|
| 350 |
+
)
|
| 351 |
+
with tool_placeholder.expander(
|
| 352 |
+
"๐ง Tool Call Information", expanded=True
|
| 353 |
+
):
|
| 354 |
+
st.markdown("".join(accumulated_tool))
|
| 355 |
+
# Process if tool_calls exists in additional_kwargs (supports various model compatibility)
|
| 356 |
+
elif (
|
| 357 |
+
hasattr(message_content, "additional_kwargs")
|
| 358 |
+
and "tool_calls" in message_content.additional_kwargs
|
| 359 |
+
):
|
| 360 |
+
tool_call_info = message_content.additional_kwargs["tool_calls"][0]
|
| 361 |
+
accumulated_tool.append("\n```json\n" + str(tool_call_info) + "\n```\n")
|
| 362 |
+
with tool_placeholder.expander(
|
| 363 |
+
"๐ง Tool Call Information", expanded=True
|
| 364 |
+
):
|
| 365 |
+
st.markdown("".join(accumulated_tool))
|
| 366 |
+
# Process if it's a tool message (tool response)
|
| 367 |
+
elif isinstance(message_content, ToolMessage):
|
| 368 |
+
accumulated_tool.append(
|
| 369 |
+
"\n```json\n" + str(message_content.content) + "\n```\n"
|
| 370 |
+
)
|
| 371 |
+
with tool_placeholder.expander("๐ง Tool Call Information", expanded=True):
|
| 372 |
+
st.markdown("".join(accumulated_tool))
|
| 373 |
+
return None
|
| 374 |
+
|
| 375 |
+
return callback_func, accumulated_text, accumulated_tool
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
async def process_query(query, text_placeholder, tool_placeholder, timeout_seconds=60):
|
| 379 |
+
"""
|
| 380 |
+
Processes user questions and generates responses.
|
| 381 |
+
|
| 382 |
+
This function passes the user's question to the agent and streams the response in real-time.
|
| 383 |
+
Returns a timeout error if the response is not completed within the specified time.
|
| 384 |
+
|
| 385 |
+
Args:
|
| 386 |
+
query: Text of the question entered by the user
|
| 387 |
+
text_placeholder: Streamlit component to display text responses
|
| 388 |
+
tool_placeholder: Streamlit component to display tool call information
|
| 389 |
+
timeout_seconds: Response generation time limit (seconds)
|
| 390 |
+
|
| 391 |
+
Returns:
|
| 392 |
+
response: Agent's response object
|
| 393 |
+
final_text: Final text response
|
| 394 |
+
final_tool: Final tool call information
|
| 395 |
+
"""
|
| 396 |
+
try:
|
| 397 |
+
if st.session_state.agent:
|
| 398 |
+
streaming_callback, accumulated_text_obj, accumulated_tool_obj = (
|
| 399 |
+
get_streaming_callback(text_placeholder, tool_placeholder)
|
| 400 |
+
)
|
| 401 |
+
try:
|
| 402 |
+
response = await asyncio.wait_for(
|
| 403 |
+
astream_graph(
|
| 404 |
+
st.session_state.agent,
|
| 405 |
+
{"messages": [HumanMessage(content=query)]},
|
| 406 |
+
callback=streaming_callback,
|
| 407 |
+
config=RunnableConfig(
|
| 408 |
+
recursion_limit=st.session_state.recursion_limit,
|
| 409 |
+
thread_id=st.session_state.thread_id,
|
| 410 |
+
),
|
| 411 |
+
),
|
| 412 |
+
timeout=timeout_seconds,
|
| 413 |
+
)
|
| 414 |
+
except asyncio.TimeoutError:
|
| 415 |
+
error_msg = f"โฑ๏ธ Request time exceeded {timeout_seconds} seconds. Please try again later."
|
| 416 |
+
return {"error": error_msg}, error_msg, ""
|
| 417 |
+
|
| 418 |
+
final_text = "".join(accumulated_text_obj)
|
| 419 |
+
final_tool = "".join(accumulated_tool_obj)
|
| 420 |
+
return response, final_text, final_tool
|
| 421 |
+
else:
|
| 422 |
+
return (
|
| 423 |
+
{"error": "๐ซ Agent has not been initialized."},
|
| 424 |
+
"๐ซ Agent has not been initialized.",
|
| 425 |
+
"",
|
| 426 |
+
)
|
| 427 |
+
except Exception as e:
|
| 428 |
+
import traceback
|
| 429 |
+
|
| 430 |
+
error_msg = f"โ Error occurred during query processing: {str(e)}\n{traceback.format_exc()}"
|
| 431 |
+
return {"error": error_msg}, error_msg, ""
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
async def initialize_session(mcp_config=None):
|
| 435 |
+
"""
|
| 436 |
+
Initializes MCP session and agent.
|
| 437 |
+
|
| 438 |
+
Args:
|
| 439 |
+
mcp_config: MCP tool configuration information (JSON). Uses default settings if None
|
| 440 |
+
|
| 441 |
+
Returns:
|
| 442 |
+
bool: Initialization success status
|
| 443 |
+
"""
|
| 444 |
+
with st.spinner("๐ Connecting to MCP server..."):
|
| 445 |
+
# First safely clean up existing client
|
| 446 |
+
await cleanup_mcp_client()
|
| 447 |
+
|
| 448 |
+
if mcp_config is None:
|
| 449 |
+
# Load settings from config.json file
|
| 450 |
+
mcp_config = load_config_from_json()
|
| 451 |
+
client = MultiServerMCPClient(mcp_config)
|
| 452 |
+
await client.__aenter__()
|
| 453 |
+
tools = client.get_tools()
|
| 454 |
+
st.session_state.tool_count = len(tools)
|
| 455 |
+
st.session_state.mcp_client = client
|
| 456 |
+
|
| 457 |
+
# Initialize appropriate model based on selection
|
| 458 |
+
selected_model = st.session_state.selected_model
|
| 459 |
+
|
| 460 |
+
if selected_model in [
|
| 461 |
+
"claude-3-7-sonnet-latest",
|
| 462 |
+
"claude-3-5-sonnet-latest",
|
| 463 |
+
"claude-3-5-haiku-latest",
|
| 464 |
+
]:
|
| 465 |
+
model = ChatAnthropic(
|
| 466 |
+
model=selected_model,
|
| 467 |
+
temperature=0.1,
|
| 468 |
+
max_tokens=OUTPUT_TOKEN_INFO[selected_model]["max_tokens"],
|
| 469 |
+
)
|
| 470 |
+
else: # Use OpenAI model
|
| 471 |
+
model = ChatOpenAI(
|
| 472 |
+
model=selected_model,
|
| 473 |
+
temperature=0.1,
|
| 474 |
+
max_tokens=OUTPUT_TOKEN_INFO[selected_model]["max_tokens"],
|
| 475 |
+
)
|
| 476 |
+
agent = create_react_agent(
|
| 477 |
+
model,
|
| 478 |
+
tools,
|
| 479 |
+
checkpointer=MemorySaver(),
|
| 480 |
+
prompt=SYSTEM_PROMPT,
|
| 481 |
+
)
|
| 482 |
+
st.session_state.agent = agent
|
| 483 |
+
st.session_state.session_initialized = True
|
| 484 |
+
return True
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
# --- Sidebar: System Settings Section ---
|
| 488 |
+
with st.sidebar:
|
| 489 |
+
st.subheader("โ๏ธ System Settings")
|
| 490 |
+
|
| 491 |
+
# Model selection feature
|
| 492 |
+
# Create list of available models
|
| 493 |
+
available_models = []
|
| 494 |
+
|
| 495 |
+
# Check Anthropic API key
|
| 496 |
+
has_anthropic_key = os.environ.get("ANTHROPIC_API_KEY") is not None
|
| 497 |
+
if has_anthropic_key:
|
| 498 |
+
available_models.extend(
|
| 499 |
+
[
|
| 500 |
+
"claude-3-7-sonnet-latest",
|
| 501 |
+
"claude-3-5-sonnet-latest",
|
| 502 |
+
"claude-3-5-haiku-latest",
|
| 503 |
+
]
|
| 504 |
+
)
|
| 505 |
+
|
| 506 |
+
# Check OpenAI API key
|
| 507 |
+
has_openai_key = os.environ.get("OPENAI_API_KEY") is not None
|
| 508 |
+
if has_openai_key:
|
| 509 |
+
available_models.extend(["gpt-4o", "gpt-4o-mini"])
|
| 510 |
+
|
| 511 |
+
# Display message if no models are available
|
| 512 |
+
if not available_models:
|
| 513 |
+
st.warning(
|
| 514 |
+
"โ ๏ธ API keys are not configured. Please add ANTHROPIC_API_KEY or OPENAI_API_KEY to your .env file."
|
| 515 |
+
)
|
| 516 |
+
# Add Claude model as default (to show UI even without keys)
|
| 517 |
+
available_models = ["claude-3-7-sonnet-latest"]
|
| 518 |
+
|
| 519 |
+
# Model selection dropdown
|
| 520 |
+
previous_model = st.session_state.selected_model
|
| 521 |
+
st.session_state.selected_model = st.selectbox(
|
| 522 |
+
"๐ค Select model to use",
|
| 523 |
+
options=available_models,
|
| 524 |
+
index=(
|
| 525 |
+
available_models.index(st.session_state.selected_model)
|
| 526 |
+
if st.session_state.selected_model in available_models
|
| 527 |
+
else 0
|
| 528 |
+
),
|
| 529 |
+
help="Anthropic models require ANTHROPIC_API_KEY and OpenAI models require OPENAI_API_KEY to be set as environment variables.",
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
# Notify when model is changed and session needs to be reinitialized
|
| 533 |
+
if (
|
| 534 |
+
previous_model != st.session_state.selected_model
|
| 535 |
+
and st.session_state.session_initialized
|
| 536 |
+
):
|
| 537 |
+
st.warning(
|
| 538 |
+
"โ ๏ธ Model has been changed. Click 'Apply Settings' button to apply changes."
|
| 539 |
+
)
|
| 540 |
+
|
| 541 |
+
# Add timeout setting slider
|
| 542 |
+
st.session_state.timeout_seconds = st.slider(
|
| 543 |
+
"โฑ๏ธ Response generation time limit (seconds)",
|
| 544 |
+
min_value=60,
|
| 545 |
+
max_value=300,
|
| 546 |
+
value=st.session_state.timeout_seconds,
|
| 547 |
+
step=10,
|
| 548 |
+
help="Set the maximum time for the agent to generate a response. Complex tasks may require more time.",
|
| 549 |
+
)
|
| 550 |
+
|
| 551 |
+
st.session_state.recursion_limit = st.slider(
|
| 552 |
+
"โฑ๏ธ Recursion call limit (count)",
|
| 553 |
+
min_value=10,
|
| 554 |
+
max_value=200,
|
| 555 |
+
value=st.session_state.recursion_limit,
|
| 556 |
+
step=10,
|
| 557 |
+
help="Set the recursion call limit. Setting too high a value may cause memory issues.",
|
| 558 |
+
)
|
| 559 |
+
|
| 560 |
+
st.divider() # Add divider
|
| 561 |
+
|
| 562 |
+
# Tool settings section
|
| 563 |
+
st.subheader("๐ง Tool Settings")
|
| 564 |
+
|
| 565 |
+
# Manage expander state in session state
|
| 566 |
+
if "mcp_tools_expander" not in st.session_state:
|
| 567 |
+
st.session_state.mcp_tools_expander = False
|
| 568 |
+
|
| 569 |
+
# MCP tool addition interface
|
| 570 |
+
with st.expander("๐งฐ Add MCP Tools", expanded=st.session_state.mcp_tools_expander):
|
| 571 |
+
# Load settings from config.json file
|
| 572 |
+
loaded_config = load_config_from_json()
|
| 573 |
+
default_config_text = json.dumps(loaded_config, indent=2, ensure_ascii=False)
|
| 574 |
+
|
| 575 |
+
# Create pending config based on existing mcp_config_text if not present
|
| 576 |
+
if "pending_mcp_config" not in st.session_state:
|
| 577 |
+
try:
|
| 578 |
+
st.session_state.pending_mcp_config = loaded_config
|
| 579 |
+
except Exception as e:
|
| 580 |
+
st.error(f"Failed to set initial pending config: {e}")
|
| 581 |
+
|
| 582 |
+
# UI for adding individual tools
|
| 583 |
+
st.subheader("Add Tool(JSON format)")
|
| 584 |
+
st.markdown(
|
| 585 |
+
"""
|
| 586 |
+
Please insert **ONE tool** in JSON format.
|
| 587 |
+
|
| 588 |
+
[How to Set Up?](https://teddylee777.notion.site/MCP-Tool-Setup-Guide-English-1d324f35d1298030a831dfb56045906a)
|
| 589 |
+
|
| 590 |
+
โ ๏ธ **Important**: JSON must be wrapped in curly braces (`{}`).
|
| 591 |
+
"""
|
| 592 |
+
)
|
| 593 |
+
|
| 594 |
+
# Provide clearer example
|
| 595 |
+
example_json = {
|
| 596 |
+
"github": {
|
| 597 |
+
"command": "npx",
|
| 598 |
+
"args": [
|
| 599 |
+
"-y",
|
| 600 |
+
"@smithery/cli@latest",
|
| 601 |
+
"run",
|
| 602 |
+
"@smithery-ai/github",
|
| 603 |
+
"--config",
|
| 604 |
+
'{"githubPersonalAccessToken":"your_token_here"}',
|
| 605 |
+
],
|
| 606 |
+
"transport": "stdio",
|
| 607 |
+
}
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
default_text = json.dumps(example_json, indent=2, ensure_ascii=False)
|
| 611 |
+
|
| 612 |
+
new_tool_json = st.text_area(
|
| 613 |
+
"Tool JSON",
|
| 614 |
+
default_text,
|
| 615 |
+
height=250,
|
| 616 |
+
)
|
| 617 |
+
|
| 618 |
+
# Add button
|
| 619 |
+
if st.button(
|
| 620 |
+
"Add Tool",
|
| 621 |
+
type="primary",
|
| 622 |
+
key="add_tool_button",
|
| 623 |
+
use_container_width=True,
|
| 624 |
+
):
|
| 625 |
+
try:
|
| 626 |
+
# Validate input
|
| 627 |
+
if not new_tool_json.strip().startswith(
|
| 628 |
+
"{"
|
| 629 |
+
) or not new_tool_json.strip().endswith("}"):
|
| 630 |
+
st.error("JSON must start and end with curly braces ({}).")
|
| 631 |
+
st.markdown('Correct format: `{ "tool_name": { ... } }`')
|
| 632 |
+
else:
|
| 633 |
+
# Parse JSON
|
| 634 |
+
parsed_tool = json.loads(new_tool_json)
|
| 635 |
+
|
| 636 |
+
# Check if it's in mcpServers format and process accordingly
|
| 637 |
+
if "mcpServers" in parsed_tool:
|
| 638 |
+
# Move contents of mcpServers to top level
|
| 639 |
+
parsed_tool = parsed_tool["mcpServers"]
|
| 640 |
+
st.info(
|
| 641 |
+
"'mcpServers' format detected. Converting automatically."
|
| 642 |
+
)
|
| 643 |
+
|
| 644 |
+
# Check number of tools entered
|
| 645 |
+
if len(parsed_tool) == 0:
|
| 646 |
+
st.error("Please enter at least one tool.")
|
| 647 |
+
else:
|
| 648 |
+
# Process all tools
|
| 649 |
+
success_tools = []
|
| 650 |
+
for tool_name, tool_config in parsed_tool.items():
|
| 651 |
+
# Check URL field and set transport
|
| 652 |
+
if "url" in tool_config:
|
| 653 |
+
# Set transport to "sse" if URL exists
|
| 654 |
+
tool_config["transport"] = "sse"
|
| 655 |
+
st.info(
|
| 656 |
+
f"URL detected in '{tool_name}' tool, setting transport to 'sse'."
|
| 657 |
+
)
|
| 658 |
+
elif "transport" not in tool_config:
|
| 659 |
+
# Set default "stdio" if URL doesn't exist and transport isn't specified
|
| 660 |
+
tool_config["transport"] = "stdio"
|
| 661 |
+
|
| 662 |
+
# Check required fields
|
| 663 |
+
if (
|
| 664 |
+
"command" not in tool_config
|
| 665 |
+
and "url" not in tool_config
|
| 666 |
+
):
|
| 667 |
+
st.error(
|
| 668 |
+
f"'{tool_name}' tool configuration requires either 'command' or 'url' field."
|
| 669 |
+
)
|
| 670 |
+
elif "command" in tool_config and "args" not in tool_config:
|
| 671 |
+
st.error(
|
| 672 |
+
f"'{tool_name}' tool configuration requires 'args' field."
|
| 673 |
+
)
|
| 674 |
+
elif "command" in tool_config and not isinstance(
|
| 675 |
+
tool_config["args"], list
|
| 676 |
+
):
|
| 677 |
+
st.error(
|
| 678 |
+
f"'args' field in '{tool_name}' tool must be an array ([]) format."
|
| 679 |
+
)
|
| 680 |
+
else:
|
| 681 |
+
# Add tool to pending_mcp_config
|
| 682 |
+
st.session_state.pending_mcp_config[tool_name] = (
|
| 683 |
+
tool_config
|
| 684 |
+
)
|
| 685 |
+
success_tools.append(tool_name)
|
| 686 |
+
|
| 687 |
+
# Success message
|
| 688 |
+
if success_tools:
|
| 689 |
+
if len(success_tools) == 1:
|
| 690 |
+
st.success(
|
| 691 |
+
f"{success_tools[0]} tool has been added. Click 'Apply Settings' button to apply."
|
| 692 |
+
)
|
| 693 |
+
else:
|
| 694 |
+
tool_names = ", ".join(success_tools)
|
| 695 |
+
st.success(
|
| 696 |
+
f"Total {len(success_tools)} tools ({tool_names}) have been added. Click 'Apply Settings' button to apply."
|
| 697 |
+
)
|
| 698 |
+
# Collapse expander after adding
|
| 699 |
+
st.session_state.mcp_tools_expander = False
|
| 700 |
+
st.rerun()
|
| 701 |
+
except json.JSONDecodeError as e:
|
| 702 |
+
st.error(f"JSON parsing error: {e}")
|
| 703 |
+
st.markdown(
|
| 704 |
+
f"""
|
| 705 |
+
**How to fix**:
|
| 706 |
+
1. Check that your JSON format is correct.
|
| 707 |
+
2. All keys must be wrapped in double quotes (").
|
| 708 |
+
3. String values must also be wrapped in double quotes (").
|
| 709 |
+
4. When using double quotes within a string, they must be escaped (\\").
|
| 710 |
+
"""
|
| 711 |
+
)
|
| 712 |
+
except Exception as e:
|
| 713 |
+
st.error(f"Error occurred: {e}")
|
| 714 |
+
|
| 715 |
+
# Display registered tools list and add delete buttons
|
| 716 |
+
with st.expander("๐ Registered Tools List", expanded=True):
|
| 717 |
+
try:
|
| 718 |
+
pending_config = st.session_state.pending_mcp_config
|
| 719 |
+
except Exception as e:
|
| 720 |
+
st.error("Not a valid MCP tool configuration.")
|
| 721 |
+
else:
|
| 722 |
+
# Iterate through keys (tool names) in pending config
|
| 723 |
+
for tool_name in list(pending_config.keys()):
|
| 724 |
+
col1, col2 = st.columns([8, 2])
|
| 725 |
+
col1.markdown(f"- **{tool_name}**")
|
| 726 |
+
if col2.button("Delete", key=f"delete_{tool_name}"):
|
| 727 |
+
# Delete tool from pending config (not applied immediately)
|
| 728 |
+
del st.session_state.pending_mcp_config[tool_name]
|
| 729 |
+
st.success(
|
| 730 |
+
f"{tool_name} tool has been deleted. Click 'Apply Settings' button to apply."
|
| 731 |
+
)
|
| 732 |
+
|
| 733 |
+
st.divider() # Add divider
|
| 734 |
+
|
| 735 |
+
# --- Sidebar: System Information and Action Buttons Section ---
|
| 736 |
+
with st.sidebar:
|
| 737 |
+
st.subheader("๐ System Information")
|
| 738 |
+
st.write(
|
| 739 |
+
f"๐ ๏ธ MCP Tools Count: {st.session_state.get('tool_count', 'Initializing...')}"
|
| 740 |
+
)
|
| 741 |
+
selected_model_name = st.session_state.selected_model
|
| 742 |
+
st.write(f"๐ง Current Model: {selected_model_name}")
|
| 743 |
+
|
| 744 |
+
# Move Apply Settings button here
|
| 745 |
+
if st.button(
|
| 746 |
+
"Apply Settings",
|
| 747 |
+
key="apply_button",
|
| 748 |
+
type="primary",
|
| 749 |
+
use_container_width=True,
|
| 750 |
+
):
|
| 751 |
+
# Display applying message
|
| 752 |
+
apply_status = st.empty()
|
| 753 |
+
with apply_status.container():
|
| 754 |
+
st.warning("๐ Applying changes. Please wait...")
|
| 755 |
+
progress_bar = st.progress(0)
|
| 756 |
+
|
| 757 |
+
# Save settings
|
| 758 |
+
st.session_state.mcp_config_text = json.dumps(
|
| 759 |
+
st.session_state.pending_mcp_config, indent=2, ensure_ascii=False
|
| 760 |
+
)
|
| 761 |
+
|
| 762 |
+
# Save settings to config.json file
|
| 763 |
+
save_result = save_config_to_json(st.session_state.pending_mcp_config)
|
| 764 |
+
if not save_result:
|
| 765 |
+
st.error("โ Failed to save settings file.")
|
| 766 |
+
|
| 767 |
+
progress_bar.progress(15)
|
| 768 |
+
|
| 769 |
+
# Prepare session initialization
|
| 770 |
+
st.session_state.session_initialized = False
|
| 771 |
+
st.session_state.agent = None
|
| 772 |
+
|
| 773 |
+
# Update progress
|
| 774 |
+
progress_bar.progress(30)
|
| 775 |
+
|
| 776 |
+
# Run initialization
|
| 777 |
+
success = st.session_state.event_loop.run_until_complete(
|
| 778 |
+
initialize_session(st.session_state.pending_mcp_config)
|
| 779 |
+
)
|
| 780 |
+
|
| 781 |
+
# Update progress
|
| 782 |
+
progress_bar.progress(100)
|
| 783 |
+
|
| 784 |
+
if success:
|
| 785 |
+
st.success("โ
New settings have been applied.")
|
| 786 |
+
# Collapse tool addition expander
|
| 787 |
+
if "mcp_tools_expander" in st.session_state:
|
| 788 |
+
st.session_state.mcp_tools_expander = False
|
| 789 |
+
else:
|
| 790 |
+
st.error("โ Failed to apply settings.")
|
| 791 |
+
|
| 792 |
+
# Refresh page
|
| 793 |
+
st.rerun()
|
| 794 |
+
|
| 795 |
+
st.divider() # Add divider
|
| 796 |
+
|
| 797 |
+
# Action buttons section
|
| 798 |
+
st.subheader("๐ Actions")
|
| 799 |
+
|
| 800 |
+
# Reset conversation button
|
| 801 |
+
if st.button("Reset Conversation", use_container_width=True, type="primary"):
|
| 802 |
+
# Reset thread_id
|
| 803 |
+
st.session_state.thread_id = random_uuid()
|
| 804 |
+
|
| 805 |
+
# Reset conversation history
|
| 806 |
+
st.session_state.history = []
|
| 807 |
+
|
| 808 |
+
# Notification message
|
| 809 |
+
st.success("โ
Conversation has been reset.")
|
| 810 |
+
|
| 811 |
+
# Refresh page
|
| 812 |
+
st.rerun()
|
| 813 |
+
|
| 814 |
+
# Show logout button only if login feature is enabled
|
| 815 |
+
if use_login and st.session_state.authenticated:
|
| 816 |
+
st.divider() # Add divider
|
| 817 |
+
if st.button("Logout", use_container_width=True, type="secondary"):
|
| 818 |
+
st.session_state.authenticated = False
|
| 819 |
+
st.success("โ
You have been logged out.")
|
| 820 |
+
st.rerun()
|
| 821 |
+
|
| 822 |
+
# --- Initialize default session (if not initialized) ---
|
| 823 |
+
if not st.session_state.session_initialized:
|
| 824 |
+
st.info(
|
| 825 |
+
"MCP server and agent are not initialized. Please click the 'Apply Settings' button in the left sidebar to initialize."
|
| 826 |
+
)
|
| 827 |
+
|
| 828 |
+
|
| 829 |
+
# --- Print conversation history ---
|
| 830 |
+
print_message()
|
| 831 |
+
|
| 832 |
+
# --- User input and processing ---
|
| 833 |
+
user_query = st.chat_input("๐ฌ Enter your question")
|
| 834 |
+
if user_query:
|
| 835 |
+
if st.session_state.session_initialized:
|
| 836 |
+
st.chat_message("user", avatar="๐งโ๐ป").markdown(user_query)
|
| 837 |
+
with st.chat_message("assistant", avatar="๐ค"):
|
| 838 |
+
tool_placeholder = st.empty()
|
| 839 |
+
text_placeholder = st.empty()
|
| 840 |
+
resp, final_text, final_tool = (
|
| 841 |
+
st.session_state.event_loop.run_until_complete(
|
| 842 |
+
process_query(
|
| 843 |
+
user_query,
|
| 844 |
+
text_placeholder,
|
| 845 |
+
tool_placeholder,
|
| 846 |
+
st.session_state.timeout_seconds,
|
| 847 |
+
)
|
| 848 |
+
)
|
| 849 |
+
)
|
| 850 |
+
if "error" in resp:
|
| 851 |
+
st.error(resp["error"])
|
| 852 |
+
else:
|
| 853 |
+
st.session_state.history.append({"role": "user", "content": user_query})
|
| 854 |
+
st.session_state.history.append(
|
| 855 |
+
{"role": "assistant", "content": final_text}
|
| 856 |
+
)
|
| 857 |
+
if final_tool.strip():
|
| 858 |
+
st.session_state.history.append(
|
| 859 |
+
{"role": "assistant_tool", "content": final_tool}
|
| 860 |
+
)
|
| 861 |
+
st.rerun()
|
| 862 |
+
else:
|
| 863 |
+
st.warning(
|
| 864 |
+
"โ ๏ธ MCP server and agent are not initialized. Please click the 'Apply Settings' button in the left sidebar to initialize."
|
| 865 |
+
)
|
app_KOR.py
ADDED
|
@@ -0,0 +1,848 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import asyncio
|
| 3 |
+
import nest_asyncio
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
import platform
|
| 7 |
+
|
| 8 |
+
if platform.system() == "Windows":
|
| 9 |
+
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
| 10 |
+
|
| 11 |
+
# nest_asyncio ์ ์ฉ: ์ด๋ฏธ ์คํ ์ค์ธ ์ด๋ฒคํธ ๋ฃจํ ๋ด์์ ์ค์ฒฉ ํธ์ถ ํ์ฉ
|
| 12 |
+
nest_asyncio.apply()
|
| 13 |
+
|
| 14 |
+
# ์ ์ญ ์ด๋ฒคํธ ๋ฃจํ ์์ฑ ๋ฐ ์ฌ์ฌ์ฉ (ํ๋ฒ ์์ฑํ ํ ๊ณ์ ์ฌ์ฉ)
|
| 15 |
+
if "event_loop" not in st.session_state:
|
| 16 |
+
loop = asyncio.new_event_loop()
|
| 17 |
+
st.session_state.event_loop = loop
|
| 18 |
+
asyncio.set_event_loop(loop)
|
| 19 |
+
|
| 20 |
+
from langgraph.prebuilt import create_react_agent
|
| 21 |
+
from langchain_anthropic import ChatAnthropic
|
| 22 |
+
from langchain_openai import ChatOpenAI
|
| 23 |
+
from langchain_core.messages import HumanMessage
|
| 24 |
+
from dotenv import load_dotenv
|
| 25 |
+
from langchain_mcp_adapters.client import MultiServerMCPClient
|
| 26 |
+
from utils import astream_graph, random_uuid
|
| 27 |
+
from langchain_core.messages.ai import AIMessageChunk
|
| 28 |
+
from langchain_core.messages.tool import ToolMessage
|
| 29 |
+
from langgraph.checkpoint.memory import MemorySaver
|
| 30 |
+
from langchain_core.runnables import RunnableConfig
|
| 31 |
+
|
| 32 |
+
# ํ๊ฒฝ ๋ณ์ ๋ก๋ (.env ํ์ผ์์ API ํค ๋ฑ์ ์ค์ ์ ๊ฐ์ ธ์ด)
|
| 33 |
+
load_dotenv(override=True)
|
| 34 |
+
|
| 35 |
+
# config.json ํ์ผ ๊ฒฝ๋ก ์ค์
|
| 36 |
+
CONFIG_FILE_PATH = "config.json"
|
| 37 |
+
|
| 38 |
+
# JSON ์ค์ ํ์ผ ๋ก๋ ํจ์
|
| 39 |
+
def load_config_from_json():
|
| 40 |
+
"""
|
| 41 |
+
config.json ํ์ผ์์ ์ค์ ์ ๋ก๋ํฉ๋๋ค.
|
| 42 |
+
ํ์ผ์ด ์๋ ๊ฒฝ์ฐ ๊ธฐ๋ณธ ์ค์ ์ผ๋ก ํ์ผ์ ์์ฑํฉ๋๋ค.
|
| 43 |
+
|
| 44 |
+
๋ฐํ๊ฐ:
|
| 45 |
+
dict: ๋ก๋๋ ์ค์
|
| 46 |
+
"""
|
| 47 |
+
default_config = {
|
| 48 |
+
"get_current_time": {
|
| 49 |
+
"command": "python",
|
| 50 |
+
"args": ["./mcp_server_time.py"],
|
| 51 |
+
"transport": "stdio"
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
try:
|
| 56 |
+
if os.path.exists(CONFIG_FILE_PATH):
|
| 57 |
+
with open(CONFIG_FILE_PATH, "r", encoding="utf-8") as f:
|
| 58 |
+
return json.load(f)
|
| 59 |
+
else:
|
| 60 |
+
# ํ์ผ์ด ์๋ ๊ฒฝ์ฐ ๊ธฐ๋ณธ ์ค์ ์ผ๋ก ํ์ผ ์์ฑ
|
| 61 |
+
save_config_to_json(default_config)
|
| 62 |
+
return default_config
|
| 63 |
+
except Exception as e:
|
| 64 |
+
st.error(f"์ค์ ํ์ผ ๋ก๋ ์ค ์ค๋ฅ ๋ฐ์: {str(e)}")
|
| 65 |
+
return default_config
|
| 66 |
+
|
| 67 |
+
# JSON ์ค์ ํ์ผ ์ ์ฅ ํจ์
|
| 68 |
+
def save_config_to_json(config):
|
| 69 |
+
"""
|
| 70 |
+
์ค์ ์ config.json ํ์ผ์ ์ ์ฅํฉ๋๋ค.
|
| 71 |
+
|
| 72 |
+
๋งค๊ฐ๋ณ์:
|
| 73 |
+
config (dict): ์ ์ฅํ ์ค์
|
| 74 |
+
|
| 75 |
+
๋ฐํ๊ฐ:
|
| 76 |
+
bool: ์ ์ฅ ์ฑ๊ณต ์ฌ๋ถ
|
| 77 |
+
"""
|
| 78 |
+
try:
|
| 79 |
+
with open(CONFIG_FILE_PATH, "w", encoding="utf-8") as f:
|
| 80 |
+
json.dump(config, f, indent=2, ensure_ascii=False)
|
| 81 |
+
return True
|
| 82 |
+
except Exception as e:
|
| 83 |
+
st.error(f"์ค์ ํ์ผ ์ ์ฅ ์ค ์ค๋ฅ ๋ฐ์: {str(e)}")
|
| 84 |
+
return False
|
| 85 |
+
|
| 86 |
+
# ๋ก๊ทธ์ธ ์ธ์
๋ณ์ ์ด๊ธฐํ
|
| 87 |
+
if "authenticated" not in st.session_state:
|
| 88 |
+
st.session_state.authenticated = False
|
| 89 |
+
|
| 90 |
+
# ๋ก๊ทธ์ธ ํ์ ์ฌ๋ถ ํ์ธ
|
| 91 |
+
use_login = os.environ.get("USE_LOGIN", "false").lower() == "true"
|
| 92 |
+
|
| 93 |
+
# ๋ก๊ทธ์ธ ์ํ์ ๋ฐ๋ผ ํ์ด์ง ์ค์ ๋ณ๊ฒฝ
|
| 94 |
+
if use_login and not st.session_state.authenticated:
|
| 95 |
+
# ๋ก๊ทธ์ธ ํ์ด์ง๋ ๊ธฐ๋ณธ(narrow) ๋ ์ด์์ ์ฌ์ฉ
|
| 96 |
+
st.set_page_config(page_title="Agent with MCP Tools", page_icon="๐ง ")
|
| 97 |
+
else:
|
| 98 |
+
# ๋ฉ์ธ ์ฑ์ wide ๋ ์ด์์ ์ฌ์ฉ
|
| 99 |
+
st.set_page_config(page_title="Agent with MCP Tools", page_icon="๐ง ", layout="wide")
|
| 100 |
+
|
| 101 |
+
# ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ด ํ์ฑํ๋์ด ์๊ณ ์์ง ์ธ์ฆ๋์ง ์์ ๊ฒฝ์ฐ ๋ก๊ทธ์ธ ํ๋ฉด ํ์
|
| 102 |
+
if use_login and not st.session_state.authenticated:
|
| 103 |
+
st.title("๐ ๋ก๊ทธ์ธ")
|
| 104 |
+
st.markdown("์์คํ
์ ์ฌ์ฉํ๋ ค๋ฉด ๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค.")
|
| 105 |
+
|
| 106 |
+
# ๋ก๊ทธ์ธ ํผ์ ํ๋ฉด ์ค์์ ์ข๊ฒ ๋ฐฐ์น
|
| 107 |
+
with st.form("login_form"):
|
| 108 |
+
username = st.text_input("์์ด๋")
|
| 109 |
+
password = st.text_input("๋น๋ฐ๋ฒํธ", type="password")
|
| 110 |
+
submit_button = st.form_submit_button("๋ก๊ทธ์ธ")
|
| 111 |
+
|
| 112 |
+
if submit_button:
|
| 113 |
+
expected_username = os.environ.get("USER_ID")
|
| 114 |
+
expected_password = os.environ.get("USER_PASSWORD")
|
| 115 |
+
|
| 116 |
+
if username == expected_username and password == expected_password:
|
| 117 |
+
st.session_state.authenticated = True
|
| 118 |
+
st.success("โ
๋ก๊ทธ์ธ ์ฑ๊ณต! ์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์...")
|
| 119 |
+
st.rerun()
|
| 120 |
+
else:
|
| 121 |
+
st.error("โ ์์ด๋ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.")
|
| 122 |
+
|
| 123 |
+
# ๋ก๊ทธ์ธ ํ๋ฉด์์๋ ๋ฉ์ธ ์ฑ์ ํ์ํ์ง ์์
|
| 124 |
+
st.stop()
|
| 125 |
+
|
| 126 |
+
# ์ฌ์ด๋๋ฐ ์ต์๋จ์ ์ ์ ์ ๋ณด ์ถ๊ฐ (๋ค๋ฅธ ์ฌ์ด๋๋ฐ ์์๋ณด๋ค ๋จผ์ ๋ฐฐ์น)
|
| 127 |
+
st.sidebar.markdown("### โ๏ธ Made by [ํ
๋๋
ธํธ](https://youtube.com/c/teddynote) ๐")
|
| 128 |
+
st.sidebar.markdown(
|
| 129 |
+
"### ๐ป [Project Page](https://github.com/teddynote-lab/langgraph-mcp-agents)"
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
st.sidebar.divider() # ๊ตฌ๋ถ์ ์ถ๊ฐ
|
| 133 |
+
|
| 134 |
+
# ๊ธฐ์กด ํ์ด์ง ํ์ดํ ๋ฐ ์ค๋ช
|
| 135 |
+
st.title("๐ฌ MCP ๋๊ตฌ ํ์ฉ ์์ด์ ํธ")
|
| 136 |
+
st.markdown("โจ MCP ๋๊ตฌ๋ฅผ ํ์ฉํ ReAct ์์ด์ ํธ์๊ฒ ์ง๋ฌธํด๋ณด์ธ์.")
|
| 137 |
+
|
| 138 |
+
SYSTEM_PROMPT = """<ROLE>
|
| 139 |
+
You are a smart agent with an ability to use tools.
|
| 140 |
+
You will be given a question and you will use the tools to answer the question.
|
| 141 |
+
Pick the most relevant tool to answer the question.
|
| 142 |
+
If you are failed to answer the question, try different tools to get context.
|
| 143 |
+
Your answer should be very polite and professional.
|
| 144 |
+
</ROLE>
|
| 145 |
+
|
| 146 |
+
----
|
| 147 |
+
|
| 148 |
+
<INSTRUCTIONS>
|
| 149 |
+
Step 1: Analyze the question
|
| 150 |
+
- Analyze user's question and final goal.
|
| 151 |
+
- If the user's question is consist of multiple sub-questions, split them into smaller sub-questions.
|
| 152 |
+
|
| 153 |
+
Step 2: Pick the most relevant tool
|
| 154 |
+
- Pick the most relevant tool to answer the question.
|
| 155 |
+
- If you are failed to answer the question, try different tools to get context.
|
| 156 |
+
|
| 157 |
+
Step 3: Answer the question
|
| 158 |
+
- Answer the question in the same language as the question.
|
| 159 |
+
- Your answer should be very polite and professional.
|
| 160 |
+
|
| 161 |
+
Step 4: Provide the source of the answer(if applicable)
|
| 162 |
+
- If you've used the tool, provide the source of the answer.
|
| 163 |
+
- Valid sources are either a website(URL) or a document(PDF, etc).
|
| 164 |
+
|
| 165 |
+
Guidelines:
|
| 166 |
+
- 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).
|
| 167 |
+
- If you've used the tool, and the source is valid URL, provide the source(URL) of the answer.
|
| 168 |
+
- Skip providing the source if the source is not URL.
|
| 169 |
+
- Answer in the same language as the question.
|
| 170 |
+
- Answer should be concise and to the point.
|
| 171 |
+
- Avoid response your output with any other information than the answer and the source.
|
| 172 |
+
</INSTRUCTIONS>
|
| 173 |
+
|
| 174 |
+
----
|
| 175 |
+
|
| 176 |
+
<OUTPUT_FORMAT>
|
| 177 |
+
(concise answer to the question)
|
| 178 |
+
|
| 179 |
+
**Source**(if applicable)
|
| 180 |
+
- (source1: valid URL)
|
| 181 |
+
- (source2: valid URL)
|
| 182 |
+
- ...
|
| 183 |
+
</OUTPUT_FORMAT>
|
| 184 |
+
"""
|
| 185 |
+
|
| 186 |
+
OUTPUT_TOKEN_INFO = {
|
| 187 |
+
"claude-3-5-sonnet-latest": {"max_tokens": 8192},
|
| 188 |
+
"claude-3-5-haiku-latest": {"max_tokens": 8192},
|
| 189 |
+
"claude-3-7-sonnet-latest": {"max_tokens": 64000},
|
| 190 |
+
"gpt-4o": {"max_tokens": 16000},
|
| 191 |
+
"gpt-4o-mini": {"max_tokens": 16000},
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
# ์ธ์
์ํ ์ด๊ธฐํ
|
| 195 |
+
if "session_initialized" not in st.session_state:
|
| 196 |
+
st.session_state.session_initialized = False # ์ธ์
์ด๊ธฐํ ์ํ ํ๋๊ทธ
|
| 197 |
+
st.session_state.agent = None # ReAct ์์ด์ ํธ ๊ฐ์ฒด ์ ์ฅ ๊ณต๊ฐ
|
| 198 |
+
st.session_state.history = [] # ๋ํ ๊ธฐ๋ก ์ ์ฅ ๋ฆฌ์คํธ
|
| 199 |
+
st.session_state.mcp_client = None # MCP ํด๋ผ์ด์ธํธ ๊ฐ์ฒด ์ ์ฅ ๊ณต๊ฐ
|
| 200 |
+
st.session_state.timeout_seconds = 120 # ์๋ต ์์ฑ ์ ํ ์๊ฐ(์ด), ๊ธฐ๋ณธ๊ฐ 120์ด
|
| 201 |
+
st.session_state.selected_model = "claude-3-7-sonnet-latest" # ๊ธฐ๋ณธ ๋ชจ๋ธ ์ ํ
|
| 202 |
+
st.session_state.recursion_limit = 100 # ์ฌ๊ท ํธ์ถ ์ ํ, ๊ธฐ๋ณธ๊ฐ 100
|
| 203 |
+
|
| 204 |
+
if "thread_id" not in st.session_state:
|
| 205 |
+
st.session_state.thread_id = random_uuid()
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
# --- ํจ์ ์ ์ ๋ถ๋ถ ---
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
async def cleanup_mcp_client():
|
| 212 |
+
"""
|
| 213 |
+
๊ธฐ์กด MCP ํด๋ผ์ด์ธํธ๋ฅผ ์์ ํ๊ฒ ์ข
๋ฃํฉ๋๋ค.
|
| 214 |
+
|
| 215 |
+
๊ธฐ์กด ํด๋ผ์ด์ธํธ๊ฐ ์๋ ๊ฒฝ์ฐ ์ ์์ ์ผ๋ก ๋ฆฌ์์ค๋ฅผ ํด์ ํฉ๋๋ค.
|
| 216 |
+
"""
|
| 217 |
+
if "mcp_client" in st.session_state and st.session_state.mcp_client is not None:
|
| 218 |
+
try:
|
| 219 |
+
|
| 220 |
+
await st.session_state.mcp_client.__aexit__(None, None, None)
|
| 221 |
+
st.session_state.mcp_client = None
|
| 222 |
+
except Exception as e:
|
| 223 |
+
import traceback
|
| 224 |
+
|
| 225 |
+
# st.warning(f"MCP ํด๋ผ์ด์ธํธ ์ข
๋ฃ ์ค ์ค๋ฅ: {str(e)}")
|
| 226 |
+
# st.warning(traceback.format_exc())
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def print_message():
|
| 230 |
+
"""
|
| 231 |
+
์ฑํ
๊ธฐ๋ก์ ํ๋ฉด์ ์ถ๋ ฅํฉ๋๋ค.
|
| 232 |
+
|
| 233 |
+
์ฌ์ฉ์์ ์ด์์คํดํธ์ ๋ฉ์์ง๋ฅผ ๊ตฌ๋ถํ์ฌ ํ๋ฉด์ ํ์ํ๊ณ ,
|
| 234 |
+
๋๊ตฌ ํธ์ถ ์ ๋ณด๋ ์ด์์คํดํธ ๋ฉ์์ง ์ปจํ
์ด๋ ๋ด์ ํ์ํฉ๋๋ค.
|
| 235 |
+
"""
|
| 236 |
+
i = 0
|
| 237 |
+
while i < len(st.session_state.history):
|
| 238 |
+
message = st.session_state.history[i]
|
| 239 |
+
|
| 240 |
+
if message["role"] == "user":
|
| 241 |
+
st.chat_message("user", avatar="๐งโ๐ป").markdown(message["content"])
|
| 242 |
+
i += 1
|
| 243 |
+
elif message["role"] == "assistant":
|
| 244 |
+
# ์ด์์คํดํธ ๋ฉ์์ง ์ปจํ
์ด๋ ์์ฑ
|
| 245 |
+
with st.chat_message("assistant", avatar="๐ค"):
|
| 246 |
+
# ์ด์์คํดํธ ๋ฉ์์ง ๋ด์ฉ ํ์
|
| 247 |
+
st.markdown(message["content"])
|
| 248 |
+
|
| 249 |
+
# ๋ค์ ๋ฉ์์ง๊ฐ ๋๊ตฌ ํธ์ถ ์ ๋ณด์ธ์ง ํ์ธ
|
| 250 |
+
if (
|
| 251 |
+
i + 1 < len(st.session_state.history)
|
| 252 |
+
and st.session_state.history[i + 1]["role"] == "assistant_tool"
|
| 253 |
+
):
|
| 254 |
+
# ๋๊ตฌ ํธ์ถ ์ ๋ณด๋ฅผ ๋์ผํ ์ปจํ
์ด๋ ๋ด์ expander๋ก ํ์
|
| 255 |
+
with st.expander("๐ง ๋๊ตฌ ํธ์ถ ์ ๋ณด", expanded=False):
|
| 256 |
+
st.markdown(st.session_state.history[i + 1]["content"])
|
| 257 |
+
i += 2 # ๋ ๋ฉ์์ง๋ฅผ ํจ๊ป ์ฒ๋ฆฌํ์ผ๋ฏ๋ก 2 ์ฆ๊ฐ
|
| 258 |
+
else:
|
| 259 |
+
i += 1 # ์ผ๋ฐ ๋ฉ์์ง๋ง ์ฒ๋ฆฌํ์ผ๋ฏ๋ก 1 ์ฆ๊ฐ
|
| 260 |
+
else:
|
| 261 |
+
# assistant_tool ๋ฉ์์ง๋ ์์์ ์ฒ๋ฆฌ๋๋ฏ๋ก ๊ฑด๋๋
|
| 262 |
+
i += 1
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def get_streaming_callback(text_placeholder, tool_placeholder):
|
| 266 |
+
"""
|
| 267 |
+
์คํธ๋ฆฌ๋ฐ ์ฝ๋ฐฑ ํจ์๋ฅผ ์์ฑํฉ๋๋ค.
|
| 268 |
+
|
| 269 |
+
์ด ํจ์๋ LLM์์ ์์ฑ๋๋ ์๋ต์ ์ค์๊ฐ์ผ๋ก ํ๋ฉด์ ํ์ํ๊ธฐ ์ํ ์ฝ๋ฐฑ ํจ์๋ฅผ ์์ฑํฉ๋๋ค.
|
| 270 |
+
ํ
์คํธ ์๋ต๊ณผ ๋๊ตฌ ํธ์ถ ์ ๋ณด๋ฅผ ๊ฐ๊ฐ ๋ค๋ฅธ ์์ญ์ ํ์ํฉ๋๋ค.
|
| 271 |
+
|
| 272 |
+
๋งค๊ฐ๋ณ์:
|
| 273 |
+
text_placeholder: ํ
์คํธ ์๋ต์ ํ์ํ Streamlit ์ปดํฌ๋ํธ
|
| 274 |
+
tool_placeholder: ๋๊ตฌ ํธ์ถ ์ ๋ณด๋ฅผ ํ์ํ Streamlit ์ปดํฌ๋ํธ
|
| 275 |
+
|
| 276 |
+
๋ฐํ๊ฐ:
|
| 277 |
+
callback_func: ์คํธ๋ฆฌ๋ฐ ์ฝ๋ฐฑ ํจ์
|
| 278 |
+
accumulated_text: ๋์ ๋ ํ
์คํธ ์๋ต์ ์ ์ฅํ๋ ๋ฆฌ์คํธ
|
| 279 |
+
accumulated_tool: ๋์ ๋ ๋๊ตฌ ํธ์ถ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ ๋ฆฌ์คํธ
|
| 280 |
+
"""
|
| 281 |
+
accumulated_text = []
|
| 282 |
+
accumulated_tool = []
|
| 283 |
+
|
| 284 |
+
def callback_func(message: dict):
|
| 285 |
+
nonlocal accumulated_text, accumulated_tool
|
| 286 |
+
message_content = message.get("content", None)
|
| 287 |
+
|
| 288 |
+
if isinstance(message_content, AIMessageChunk):
|
| 289 |
+
content = message_content.content
|
| 290 |
+
# ์ฝํ
์ธ ๊ฐ ๋ฆฌ์คํธ ํํ์ธ ๊ฒฝ์ฐ (Claude ๋ชจ๋ธ ๋ฑ์์ ์ฃผ๋ก ๋ฐ์)
|
| 291 |
+
if isinstance(content, list) and len(content) > 0:
|
| 292 |
+
message_chunk = content[0]
|
| 293 |
+
# ํ
์คํธ ํ์
์ธ ๊ฒฝ์ฐ ์ฒ๋ฆฌ
|
| 294 |
+
if message_chunk["type"] == "text":
|
| 295 |
+
accumulated_text.append(message_chunk["text"])
|
| 296 |
+
text_placeholder.markdown("".join(accumulated_text))
|
| 297 |
+
# ๋๊ตฌ ์ฌ์ฉ ํ์
์ธ ๊ฒฝ์ฐ ์ฒ๋ฆฌ
|
| 298 |
+
elif message_chunk["type"] == "tool_use":
|
| 299 |
+
if "partial_json" in message_chunk:
|
| 300 |
+
accumulated_tool.append(message_chunk["partial_json"])
|
| 301 |
+
else:
|
| 302 |
+
tool_call_chunks = message_content.tool_call_chunks
|
| 303 |
+
tool_call_chunk = tool_call_chunks[0]
|
| 304 |
+
accumulated_tool.append(
|
| 305 |
+
"\n```json\n" + str(tool_call_chunk) + "\n```\n"
|
| 306 |
+
)
|
| 307 |
+
with tool_placeholder.expander("๐ง ๋๊ตฌ ํธ์ถ ์ ๋ณด", expanded=True):
|
| 308 |
+
st.markdown("".join(accumulated_tool))
|
| 309 |
+
# tool_calls ์์ฑ์ด ์๋ ๊ฒฝ์ฐ ์ฒ๋ฆฌ (OpenAI ๋ชจ๋ธ ๋ฑ์์ ์ฃผ๋ก ๋ฐ์)
|
| 310 |
+
elif (
|
| 311 |
+
hasattr(message_content, "tool_calls")
|
| 312 |
+
and message_content.tool_calls
|
| 313 |
+
and len(message_content.tool_calls[0]["name"]) > 0
|
| 314 |
+
):
|
| 315 |
+
tool_call_info = message_content.tool_calls[0]
|
| 316 |
+
accumulated_tool.append("\n```json\n" + str(tool_call_info) + "\n```\n")
|
| 317 |
+
with tool_placeholder.expander("๐ง ๋๊ตฌ ํธ์ถ ์ ๋ณด", expanded=True):
|
| 318 |
+
st.markdown("".join(accumulated_tool))
|
| 319 |
+
# ๋จ์ ๋ฌธ์์ด์ธ ๊ฒฝ์ฐ ์ฒ๋ฆฌ
|
| 320 |
+
elif isinstance(content, str):
|
| 321 |
+
accumulated_text.append(content)
|
| 322 |
+
text_placeholder.markdown("".join(accumulated_text))
|
| 323 |
+
# ์ ํจํ์ง ์์ ๋๊ตฌ ํธ์ถ ์ ๋ณด๊ฐ ์๋ ๊ฒฝ์ฐ ์ฒ๋ฆฌ
|
| 324 |
+
elif (
|
| 325 |
+
hasattr(message_content, "invalid_tool_calls")
|
| 326 |
+
and message_content.invalid_tool_calls
|
| 327 |
+
):
|
| 328 |
+
tool_call_info = message_content.invalid_tool_calls[0]
|
| 329 |
+
accumulated_tool.append("\n```json\n" + str(tool_call_info) + "\n```\n")
|
| 330 |
+
with tool_placeholder.expander(
|
| 331 |
+
"๐ง ๋๊ตฌ ํธ์ถ ์ ๋ณด (์ ํจํ์ง ์์)", expanded=True
|
| 332 |
+
):
|
| 333 |
+
st.markdown("".join(accumulated_tool))
|
| 334 |
+
# tool_call_chunks ์์ฑ์ด ์๋ ๊ฒฝ์ฐ ์ฒ๋ฆฌ
|
| 335 |
+
elif (
|
| 336 |
+
hasattr(message_content, "tool_call_chunks")
|
| 337 |
+
and message_content.tool_call_chunks
|
| 338 |
+
):
|
| 339 |
+
tool_call_chunk = message_content.tool_call_chunks[0]
|
| 340 |
+
accumulated_tool.append(
|
| 341 |
+
"\n```json\n" + str(tool_call_chunk) + "\n```\n"
|
| 342 |
+
)
|
| 343 |
+
with tool_placeholder.expander("๐ง ๋๊ตฌ ํธ์ถ ์ ๋ณด", expanded=True):
|
| 344 |
+
st.markdown("".join(accumulated_tool))
|
| 345 |
+
# additional_kwargs์ tool_calls๊ฐ ์๋ ๊ฒฝ์ฐ ์ฒ๋ฆฌ (๋ค์ํ ๋ชจ๋ธ ํธํ์ฑ ์ง์)
|
| 346 |
+
elif (
|
| 347 |
+
hasattr(message_content, "additional_kwargs")
|
| 348 |
+
and "tool_calls" in message_content.additional_kwargs
|
| 349 |
+
):
|
| 350 |
+
tool_call_info = message_content.additional_kwargs["tool_calls"][0]
|
| 351 |
+
accumulated_tool.append("\n```json\n" + str(tool_call_info) + "\n```\n")
|
| 352 |
+
with tool_placeholder.expander("๐ง ๋๊ตฌ ํธ์ถ ์ ๋ณด", expanded=True):
|
| 353 |
+
st.markdown("".join(accumulated_tool))
|
| 354 |
+
# ๋๊ตฌ ๋ฉ์์ง์ธ ๊ฒฝ์ฐ ์ฒ๋ฆฌ (๋๊ตฌ์ ์๋ต)
|
| 355 |
+
elif isinstance(message_content, ToolMessage):
|
| 356 |
+
accumulated_tool.append(
|
| 357 |
+
"\n```json\n" + str(message_content.content) + "\n```\n"
|
| 358 |
+
)
|
| 359 |
+
with tool_placeholder.expander("๐ง ๋๊ตฌ ํธ๏ฟฝ๏ฟฝ๏ฟฝ ์ ๋ณด", expanded=True):
|
| 360 |
+
st.markdown("".join(accumulated_tool))
|
| 361 |
+
return None
|
| 362 |
+
|
| 363 |
+
return callback_func, accumulated_text, accumulated_tool
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
async def process_query(query, text_placeholder, tool_placeholder, timeout_seconds=60):
|
| 367 |
+
"""
|
| 368 |
+
์ฌ์ฉ์ ์ง๋ฌธ์ ์ฒ๋ฆฌํ๊ณ ์๋ต์ ์์ฑํฉ๋๋ค.
|
| 369 |
+
|
| 370 |
+
์ด ํจ์๋ ์ฌ์ฉ์์ ์ง๋ฌธ์ ์์ด์ ํธ์ ์ ๋ฌํ๊ณ , ์๋ต์ ์ค์๊ฐ์ผ๋ก ์คํธ๋ฆฌ๋ฐํ์ฌ ํ์ํฉ๋๋ค.
|
| 371 |
+
์ง์ ๋ ์๊ฐ ๋ด์ ์๋ต์ด ์๋ฃ๋์ง ์์ผ๋ฉด ํ์์์ ์ค๋ฅ๋ฅผ ๋ฐํํฉ๋๋ค.
|
| 372 |
+
|
| 373 |
+
๋งค๊ฐ๋ณ์:
|
| 374 |
+
query: ์ฌ์ฉ์๊ฐ ์
๋ ฅํ ์ง๋ฌธ ํ
์คํธ
|
| 375 |
+
text_placeholder: ํ
์คํธ ์๋ต์ ํ์ํ Streamlit ์ปดํฌ๋ํธ
|
| 376 |
+
tool_placeholder: ๋๊ตฌ ํธ์ถ ์ ๋ณด๋ฅผ ํ์ํ Streamlit ์ปดํฌ๋ํธ
|
| 377 |
+
timeout_seconds: ์๋ต ์์ฑ ์ ํ ์๊ฐ(์ด)
|
| 378 |
+
|
| 379 |
+
๋ฐํ๊ฐ:
|
| 380 |
+
response: ์์ด์ ํธ์ ์๋ต ๊ฐ์ฒด
|
| 381 |
+
final_text: ์ต์ข
ํ
์คํธ ์๋ต
|
| 382 |
+
final_tool: ์ต์ข
๋๊ตฌ ํธ์ถ ์ ๋ณด
|
| 383 |
+
"""
|
| 384 |
+
try:
|
| 385 |
+
if st.session_state.agent:
|
| 386 |
+
streaming_callback, accumulated_text_obj, accumulated_tool_obj = (
|
| 387 |
+
get_streaming_callback(text_placeholder, tool_placeholder)
|
| 388 |
+
)
|
| 389 |
+
try:
|
| 390 |
+
response = await asyncio.wait_for(
|
| 391 |
+
astream_graph(
|
| 392 |
+
st.session_state.agent,
|
| 393 |
+
{"messages": [HumanMessage(content=query)]},
|
| 394 |
+
callback=streaming_callback,
|
| 395 |
+
config=RunnableConfig(
|
| 396 |
+
recursion_limit=st.session_state.recursion_limit,
|
| 397 |
+
thread_id=st.session_state.thread_id,
|
| 398 |
+
),
|
| 399 |
+
),
|
| 400 |
+
timeout=timeout_seconds,
|
| 401 |
+
)
|
| 402 |
+
except asyncio.TimeoutError:
|
| 403 |
+
error_msg = f"โฑ๏ธ ์์ฒญ ์๊ฐ์ด {timeout_seconds}์ด๋ฅผ ์ด๊ณผํ์ต๋๋ค. ๋์ค์ ๋ค์ ์๋ํด ์ฃผ์ธ์."
|
| 404 |
+
return {"error": error_msg}, error_msg, ""
|
| 405 |
+
|
| 406 |
+
final_text = "".join(accumulated_text_obj)
|
| 407 |
+
final_tool = "".join(accumulated_tool_obj)
|
| 408 |
+
return response, final_text, final_tool
|
| 409 |
+
else:
|
| 410 |
+
return (
|
| 411 |
+
{"error": "๐ซ ์์ด์ ํธ๊ฐ ์ด๊ธฐํ๋์ง ์์์ต๋๋ค."},
|
| 412 |
+
"๐ซ ์์ด์ ํธ๊ฐ ์ด๊ธฐํ๋์ง ์์์ต๋๋ค.",
|
| 413 |
+
"",
|
| 414 |
+
)
|
| 415 |
+
except Exception as e:
|
| 416 |
+
import traceback
|
| 417 |
+
|
| 418 |
+
error_msg = f"โ ์ฟผ๋ฆฌ ์ฒ๋ฆฌ ์ค ์ค๋ฅ ๋ฐ์: {str(e)}\n{traceback.format_exc()}"
|
| 419 |
+
return {"error": error_msg}, error_msg, ""
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
async def initialize_session(mcp_config=None):
|
| 423 |
+
"""
|
| 424 |
+
MCP ์ธ์
๊ณผ ์์ด์ ํธ๋ฅผ ์ด๊ธฐํํฉ๋๋ค.
|
| 425 |
+
|
| 426 |
+
๋งค๊ฐ๋ณ์:
|
| 427 |
+
mcp_config: MCP ๋๊ตฌ ์ค์ ์ ๋ณด(JSON). None์ธ ๊ฒฝ์ฐ ๊ธฐ๋ณธ ์ค์ ์ฌ์ฉ
|
| 428 |
+
|
| 429 |
+
๋ฐํ๊ฐ:
|
| 430 |
+
bool: ์ด๊ธฐํ ์ฑ๊ณต ์ฌ๋ถ
|
| 431 |
+
"""
|
| 432 |
+
with st.spinner("๐ MCP ์๋ฒ์ ์ฐ๊ฒฐ ์ค..."):
|
| 433 |
+
# ๋จผ์ ๊ธฐ์กด ํด๋ผ์ด์ธํธ๋ฅผ ์์ ํ๊ฒ ์ ๋ฆฌ
|
| 434 |
+
await cleanup_mcp_client()
|
| 435 |
+
|
| 436 |
+
if mcp_config is None:
|
| 437 |
+
# config.json ํ์ผ์์ ์ค์ ๋ก๋
|
| 438 |
+
mcp_config = load_config_from_json()
|
| 439 |
+
client = MultiServerMCPClient(mcp_config)
|
| 440 |
+
await client.__aenter__()
|
| 441 |
+
tools = client.get_tools()
|
| 442 |
+
st.session_state.tool_count = len(tools)
|
| 443 |
+
st.session_state.mcp_client = client
|
| 444 |
+
|
| 445 |
+
# ์ ํ๋ ๋ชจ๋ธ์ ๋ฐ๋ผ ์ ์ ํ ๋ชจ๋ธ ์ด๊ธฐํ
|
| 446 |
+
selected_model = st.session_state.selected_model
|
| 447 |
+
|
| 448 |
+
if selected_model in [
|
| 449 |
+
"claude-3-7-sonnet-latest",
|
| 450 |
+
"claude-3-5-sonnet-latest",
|
| 451 |
+
"claude-3-5-haiku-latest",
|
| 452 |
+
]:
|
| 453 |
+
model = ChatAnthropic(
|
| 454 |
+
model=selected_model,
|
| 455 |
+
temperature=0.1,
|
| 456 |
+
max_tokens=OUTPUT_TOKEN_INFO[selected_model]["max_tokens"],
|
| 457 |
+
)
|
| 458 |
+
else: # OpenAI ๋ชจ๋ธ ์ฌ์ฉ
|
| 459 |
+
model = ChatOpenAI(
|
| 460 |
+
model=selected_model,
|
| 461 |
+
temperature=0.1,
|
| 462 |
+
max_tokens=OUTPUT_TOKEN_INFO[selected_model]["max_tokens"],
|
| 463 |
+
)
|
| 464 |
+
agent = create_react_agent(
|
| 465 |
+
model,
|
| 466 |
+
tools,
|
| 467 |
+
checkpointer=MemorySaver(),
|
| 468 |
+
prompt=SYSTEM_PROMPT,
|
| 469 |
+
)
|
| 470 |
+
st.session_state.agent = agent
|
| 471 |
+
st.session_state.session_initialized = True
|
| 472 |
+
return True
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
# --- ์ฌ์ด๋๋ฐ: ์์คํ
์ค์ ์น์
---
|
| 476 |
+
with st.sidebar:
|
| 477 |
+
st.subheader("โ๏ธ ์์คํ
์ค์ ")
|
| 478 |
+
|
| 479 |
+
# ๋ชจ๋ธ ์ ํ ๊ธฐ๋ฅ
|
| 480 |
+
# ์ฌ์ฉ ๊ฐ๋ฅํ ๋ชจ๋ธ ๋ชฉ๋ก ์์ฑ
|
| 481 |
+
available_models = []
|
| 482 |
+
|
| 483 |
+
# Anthropic API ํค ํ์ธ
|
| 484 |
+
has_anthropic_key = os.environ.get("ANTHROPIC_API_KEY") is not None
|
| 485 |
+
if has_anthropic_key:
|
| 486 |
+
available_models.extend(
|
| 487 |
+
[
|
| 488 |
+
"claude-3-7-sonnet-latest",
|
| 489 |
+
"claude-3-5-sonnet-latest",
|
| 490 |
+
"claude-3-5-haiku-latest",
|
| 491 |
+
]
|
| 492 |
+
)
|
| 493 |
+
|
| 494 |
+
# OpenAI API ํค ํ์ธ
|
| 495 |
+
has_openai_key = os.environ.get("OPENAI_API_KEY") is not None
|
| 496 |
+
if has_openai_key:
|
| 497 |
+
available_models.extend(["gpt-4o", "gpt-4o-mini"])
|
| 498 |
+
|
| 499 |
+
# ์ฌ์ฉ ๊ฐ๋ฅํ ๋ชจ๋ธ์ด ์๋ ๊ฒฝ์ฐ ๋ฉ์์ง ํ์
|
| 500 |
+
if not available_models:
|
| 501 |
+
st.warning(
|
| 502 |
+
"โ ๏ธ API ํค๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค. .env ํ์ผ์ ANTHROPIC_API_KEY ๋๋ OPENAI_API_KEY๋ฅผ ์ถ๊ฐํด์ฃผ์ธ์."
|
| 503 |
+
)
|
| 504 |
+
# ๊ธฐ๋ณธ๊ฐ์ผ๋ก Claude ๋ชจ๋ธ ์ถ๊ฐ (ํค๊ฐ ์์ด๋ UI๋ฅผ ๋ณด์ฌ์ฃผ๊ธฐ ์ํจ)
|
| 505 |
+
available_models = ["claude-3-7-sonnet-latest"]
|
| 506 |
+
|
| 507 |
+
# ๋ชจ๋ธ ์ ํ ๋๋กญ๋ค์ด
|
| 508 |
+
previous_model = st.session_state.selected_model
|
| 509 |
+
st.session_state.selected_model = st.selectbox(
|
| 510 |
+
"๐ค ์ฌ์ฉํ ๋ชจ๋ธ ์ ํ",
|
| 511 |
+
options=available_models,
|
| 512 |
+
index=(
|
| 513 |
+
available_models.index(st.session_state.selected_model)
|
| 514 |
+
if st.session_state.selected_model in available_models
|
| 515 |
+
else 0
|
| 516 |
+
),
|
| 517 |
+
help="Anthropic ๋ชจ๋ธ์ ANTHROPIC_API_KEY๊ฐ, OpenAI ๋ชจ๋ธ์ OPENAI_API_KEY๊ฐ ํ๊ฒฝ๋ณ์๋ก ์ค์ ๋์ด์ผ ํฉ๋๋ค.",
|
| 518 |
+
)
|
| 519 |
+
|
| 520 |
+
# ๋ชจ๋ธ์ด ๋ณ๊ฒฝ๋์์ ๋ ์ธ์
์ด๊ธฐํ ํ์ ์๋ฆผ
|
| 521 |
+
if (
|
| 522 |
+
previous_model != st.session_state.selected_model
|
| 523 |
+
and st.session_state.session_initialized
|
| 524 |
+
):
|
| 525 |
+
st.warning(
|
| 526 |
+
"โ ๏ธ ๋ชจ๋ธ์ด ๋ณ๊ฒฝ๋์์ต๋๋ค. '์ค์ ์ ์ฉํ๊ธฐ' ๋ฒํผ์ ๋๋ฌ ๋ณ๊ฒฝ์ฌํญ์ ์ ์ฉํ์ธ์."
|
| 527 |
+
)
|
| 528 |
+
|
| 529 |
+
# ํ์์์ ์ค์ ์ฌ๋ผ์ด๋ ์ถ๊ฐ
|
| 530 |
+
st.session_state.timeout_seconds = st.slider(
|
| 531 |
+
"โฑ๏ธ ์๋ต ์์ฑ ์ ํ ์๊ฐ(์ด)",
|
| 532 |
+
min_value=60,
|
| 533 |
+
max_value=300,
|
| 534 |
+
value=st.session_state.timeout_seconds,
|
| 535 |
+
step=10,
|
| 536 |
+
help="์์ด์ ํธ๊ฐ ์๋ต์ ์์ฑํ๋ ์ต๋ ์๊ฐ์ ์ค์ ํฉ๋๋ค. ๋ณต์กํ ์์
์ ๋ ๊ธด ์๊ฐ์ด ํ์ํ ์ ์์ต๋๋ค.",
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
st.session_state.recursion_limit = st.slider(
|
| 540 |
+
"โฑ๏ธ ์ฌ๊ท ํธ์ถ ์ ํ(ํ์)",
|
| 541 |
+
min_value=10,
|
| 542 |
+
max_value=200,
|
| 543 |
+
value=st.session_state.recursion_limit,
|
| 544 |
+
step=10,
|
| 545 |
+
help="์ฌ๊ท ํธ์ถ ์ ํ ํ์๋ฅผ ์ค์ ํฉ๋๋ค. ๋๋ฌด ๋์ ๊ฐ์ ์ค์ ํ๋ฉด ๋ฉ๋ชจ๋ฆฌ ๋ถ์กฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.",
|
| 546 |
+
)
|
| 547 |
+
|
| 548 |
+
st.divider() # ๊ตฌ๋ถ์ ์ถ๊ฐ
|
| 549 |
+
|
| 550 |
+
# ๋๊ตฌ ์ค์ ์น์
์ถ๊ฐ
|
| 551 |
+
st.subheader("๐ง ๋๊ตฌ ์ค์ ")
|
| 552 |
+
|
| 553 |
+
# expander ์ํ๋ฅผ ์ธ์
์ํ๋ก ๊ด๋ฆฌ
|
| 554 |
+
if "mcp_tools_expander" not in st.session_state:
|
| 555 |
+
st.session_state.mcp_tools_expander = False
|
| 556 |
+
|
| 557 |
+
# MCP ๋๊ตฌ ์ถ๊ฐ ์ธํฐํ์ด์ค
|
| 558 |
+
with st.expander("๐งฐ MCP ๋๊ตฌ ์ถ๊ฐ", expanded=st.session_state.mcp_tools_expander):
|
| 559 |
+
# config.json ํ์ผ์์ ์ค์ ๋ก๋ํ์ฌ ํ์
|
| 560 |
+
loaded_config = load_config_from_json()
|
| 561 |
+
default_config_text = json.dumps(loaded_config, indent=2, ensure_ascii=False)
|
| 562 |
+
|
| 563 |
+
# pending config๊ฐ ์์ผ๋ฉด ๊ธฐ์กด mcp_config_text ๊ธฐ๋ฐ์ผ๋ก ์์ฑ
|
| 564 |
+
if "pending_mcp_config" not in st.session_state:
|
| 565 |
+
try:
|
| 566 |
+
st.session_state.pending_mcp_config = loaded_config
|
| 567 |
+
except Exception as e:
|
| 568 |
+
st.error(f"์ด๊ธฐ pending config ์ค์ ์คํจ: {e}")
|
| 569 |
+
|
| 570 |
+
# ๊ฐ๋ณ ๋๊ตฌ ์ถ๊ฐ๋ฅผ ์ํ UI
|
| 571 |
+
st.subheader("๋๊ตฌ ์ถ๊ฐ")
|
| 572 |
+
st.markdown(
|
| 573 |
+
"""
|
| 574 |
+
[์ด๋ป๊ฒ ์ค์ ํ๋์?](https://teddylee777.notion.site/MCP-1d324f35d12980c8b018e12afdf545a1?pvs=4)
|
| 575 |
+
|
| 576 |
+
โ ๏ธ **์ค์**: JSON์ ๋ฐ๋์ ์ค๊ดํธ(`{}`)๋ก ๊ฐ์ธ์ผ ํฉ๋๋ค."""
|
| 577 |
+
)
|
| 578 |
+
|
| 579 |
+
# ๋ณด๋ค ๋ช
ํํ ์์ ์ ๊ณต
|
| 580 |
+
example_json = {
|
| 581 |
+
"github": {
|
| 582 |
+
"command": "npx",
|
| 583 |
+
"args": [
|
| 584 |
+
"-y",
|
| 585 |
+
"@smithery/cli@latest",
|
| 586 |
+
"run",
|
| 587 |
+
"@smithery-ai/github",
|
| 588 |
+
"--config",
|
| 589 |
+
'{"githubPersonalAccessToken":"your_token_here"}',
|
| 590 |
+
],
|
| 591 |
+
"transport": "stdio",
|
| 592 |
+
}
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
default_text = json.dumps(example_json, indent=2, ensure_ascii=False)
|
| 596 |
+
|
| 597 |
+
new_tool_json = st.text_area(
|
| 598 |
+
"๋๊ตฌ JSON",
|
| 599 |
+
default_text,
|
| 600 |
+
height=250,
|
| 601 |
+
)
|
| 602 |
+
|
| 603 |
+
# ์ถ๊ฐํ๊ธฐ ๋ฒํผ
|
| 604 |
+
if st.button(
|
| 605 |
+
"๋๊ตฌ ์ถ๊ฐ",
|
| 606 |
+
type="primary",
|
| 607 |
+
key="add_tool_button",
|
| 608 |
+
use_container_width=True,
|
| 609 |
+
):
|
| 610 |
+
try:
|
| 611 |
+
# ์
๋ ฅ๊ฐ ๊ฒ์ฆ
|
| 612 |
+
if not new_tool_json.strip().startswith(
|
| 613 |
+
"{"
|
| 614 |
+
) or not new_tool_json.strip().endswith("}"):
|
| 615 |
+
st.error("JSON์ ์ค๊ดํธ({})๋ก ์์ํ๊ณ ๋๋์ผ ํฉ๋๋ค.")
|
| 616 |
+
st.markdown('์ฌ๋ฐ๋ฅธ ํ์: `{ "๋๊ตฌ์ด๋ฆ": { ... } }`')
|
| 617 |
+
else:
|
| 618 |
+
# JSON ํ์ฑ
|
| 619 |
+
parsed_tool = json.loads(new_tool_json)
|
| 620 |
+
|
| 621 |
+
# mcpServers ํ์์ธ์ง ํ์ธํ๊ณ ์ฒ๋ฆฌ
|
| 622 |
+
if "mcpServers" in parsed_tool:
|
| 623 |
+
# mcpServers ์์ ๋ด์ฉ์ ์ต์์๋ก ์ด๋
|
| 624 |
+
parsed_tool = parsed_tool["mcpServers"]
|
| 625 |
+
st.info(
|
| 626 |
+
"'mcpServers' ํ์์ด ๊ฐ์ง๋์์ต๋๋ค. ์๋์ผ๋ก ๋ณํํฉ๋๋ค."
|
| 627 |
+
)
|
| 628 |
+
|
| 629 |
+
# ์
๋ ฅ๋ ๋๊ตฌ ์ ํ์ธ
|
| 630 |
+
if len(parsed_tool) == 0:
|
| 631 |
+
st.error("์ต์ ํ๋ ์ด์์ ๋๊ตฌ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.")
|
| 632 |
+
else:
|
| 633 |
+
# ๋ชจ๋ ๋๊ตฌ์ ๋ํด ์ฒ๋ฆฌ
|
| 634 |
+
success_tools = []
|
| 635 |
+
for tool_name, tool_config in parsed_tool.items():
|
| 636 |
+
# URL ํ๋ ํ์ธ ๋ฐ transport ์ค์
|
| 637 |
+
if "url" in tool_config:
|
| 638 |
+
# URL์ด ์๋ ๊ฒฝ์ฐ transport๋ฅผ "sse"๋ก ์ค์
|
| 639 |
+
tool_config["transport"] = "sse"
|
| 640 |
+
st.info(
|
| 641 |
+
f"'{tool_name}' ๋๊ตฌ์ URL์ด ๊ฐ์ง๋์ด transport๋ฅผ 'sse'๋ก ์ค์ ํ์ต๋๋ค."
|
| 642 |
+
)
|
| 643 |
+
elif "transport" not in tool_config:
|
| 644 |
+
# URL์ด ์๊ณ transport๋ ์๋ ๊ฒฝ์ฐ ๊ธฐ๋ณธ๊ฐ "stdio" ์ค์
|
| 645 |
+
tool_config["transport"] = "stdio"
|
| 646 |
+
|
| 647 |
+
# ํ์ ํ๋ ํ์ธ
|
| 648 |
+
if (
|
| 649 |
+
"command" not in tool_config
|
| 650 |
+
and "url" not in tool_config
|
| 651 |
+
):
|
| 652 |
+
st.error(
|
| 653 |
+
f"'{tool_name}' ๋๊ตฌ ์ค์ ์๋ 'command' ๋๋ 'url' ํ๋๊ฐ ํ์ํฉ๋๋ค."
|
| 654 |
+
)
|
| 655 |
+
elif "command" in tool_config and "args" not in tool_config:
|
| 656 |
+
st.error(
|
| 657 |
+
f"'{tool_name}' ๋๊ตฌ ์ค์ ์๋ 'args' ํ๋๊ฐ ํ์ํฉ๋๋ค."
|
| 658 |
+
)
|
| 659 |
+
elif "command" in tool_config and not isinstance(
|
| 660 |
+
tool_config["args"], list
|
| 661 |
+
):
|
| 662 |
+
st.error(
|
| 663 |
+
f"'{tool_name}' ๋๊ตฌ์ 'args' ํ๋๋ ๋ฐ๋์ ๋ฐฐ์ด([]) ํ์์ด์ด์ผ ํฉ๋๋ค."
|
| 664 |
+
)
|
| 665 |
+
else:
|
| 666 |
+
# pending_mcp_config์ ๋๊ตฌ ์ถ๊ฐ
|
| 667 |
+
st.session_state.pending_mcp_config[tool_name] = (
|
| 668 |
+
tool_config
|
| 669 |
+
)
|
| 670 |
+
success_tools.append(tool_name)
|
| 671 |
+
|
| 672 |
+
# ์ฑ๊ณต ๋ฉ์์ง
|
| 673 |
+
if success_tools:
|
| 674 |
+
if len(success_tools) == 1:
|
| 675 |
+
st.success(
|
| 676 |
+
f"{success_tools[0]} ๋๊ตฌ๊ฐ ์ถ๊ฐ๋์์ต๋๋ค. ์ ์ฉํ๋ ค๋ฉด '์ค์ ์ ์ฉํ๊ธฐ' ๋ฒํผ์ ๋๋ฌ์ฃผ์ธ์."
|
| 677 |
+
)
|
| 678 |
+
else:
|
| 679 |
+
tool_names = ", ".join(success_tools)
|
| 680 |
+
st.success(
|
| 681 |
+
f"์ด {len(success_tools)}๊ฐ ๋๊ตฌ({tool_names})๊ฐ ์ถ๊ฐ๋์์ต๋๋ค. ์ ์ฉํ๋ ค๋ฉด '์ค์ ์ ์ฉํ๊ธฐ' ๋ฒํผ์ ๋๋ฌ์ฃผ์ธ์."
|
| 682 |
+
)
|
| 683 |
+
# ์ถ๊ฐ๋๋ฉด expander๋ฅผ ์ ์ด์ค
|
| 684 |
+
st.session_state.mcp_tools_expander = False
|
| 685 |
+
st.rerun()
|
| 686 |
+
except json.JSONDecodeError as e:
|
| 687 |
+
st.error(f"JSON ํ์ฑ ์๋ฌ: {e}")
|
| 688 |
+
st.markdown(
|
| 689 |
+
f"""
|
| 690 |
+
**์์ ๋ฐฉ๋ฒ**:
|
| 691 |
+
1. JSON ํ์์ด ์ฌ๋ฐ๋ฅธ์ง ํ์ธํ์ธ์.
|
| 692 |
+
2. ๋ชจ๋ ํค๋ ํฐ๋ฐ์ดํ(")๋ก ๊ฐ์ธ์ผ ํฉ๋๋ค.
|
| 693 |
+
3. ๋ฌธ์์ด ๊ฐ๋ ํฐ๋ฐ์ดํ(")๋ก ๊ฐ์ธ์ผ ํฉ๋๋ค.
|
| 694 |
+
4. ๋ฌธ์์ด ๋ด์์ ํฐ๋ฐ์ดํ๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ ์ด์ค์ผ์ดํ(\\")ํด์ผ ํฉ๋๋ค.
|
| 695 |
+
"""
|
| 696 |
+
)
|
| 697 |
+
except Exception as e:
|
| 698 |
+
st.error(f"์ค๋ฅ ๋ฐ์: {e}")
|
| 699 |
+
|
| 700 |
+
# ๋ฑ๋ก๋ ๋๊ตฌ ๋ชฉ๋ก ํ์ ๋ฐ ์ญ์ ๋ฒํผ ์ถ๊ฐ
|
| 701 |
+
with st.expander("๐ ๋ฑ๋ก๋ ๋๊ตฌ ๋ชฉ๋ก", expanded=True):
|
| 702 |
+
try:
|
| 703 |
+
pending_config = st.session_state.pending_mcp_config
|
| 704 |
+
except Exception as e:
|
| 705 |
+
st.error("์ ํจํ MCP ๋๊ตฌ ์ค์ ์ด ์๋๋๋ค.")
|
| 706 |
+
else:
|
| 707 |
+
# pending config์ ํค(๋๊ตฌ ์ด๋ฆ) ๋ชฉ๋ก์ ์ํํ๋ฉฐ ํ์
|
| 708 |
+
for tool_name in list(pending_config.keys()):
|
| 709 |
+
col1, col2 = st.columns([8, 2])
|
| 710 |
+
col1.markdown(f"- **{tool_name}**")
|
| 711 |
+
if col2.button("์ญ์ ", key=f"delete_{tool_name}"):
|
| 712 |
+
# pending config์์ ํด๋น ๋๊ตฌ ์ญ์ (์ฆ์ ๏ฟฝ๏ฟฝ์ฉ๋์ง๋ ์์)
|
| 713 |
+
del st.session_state.pending_mcp_config[tool_name]
|
| 714 |
+
st.success(
|
| 715 |
+
f"{tool_name} ๋๊ตฌ๊ฐ ์ญ์ ๋์์ต๋๋ค. ์ ์ฉํ๋ ค๋ฉด '์ค์ ์ ์ฉํ๊ธฐ' ๋ฒํผ์ ๋๋ฌ์ฃผ์ธ์."
|
| 716 |
+
)
|
| 717 |
+
|
| 718 |
+
st.divider() # ๊ตฌ๋ถ์ ์ถ๊ฐ
|
| 719 |
+
|
| 720 |
+
# --- ์ฌ์ด๋๋ฐ: ์์คํ
์ ๋ณด ๋ฐ ์์
๋ฒํผ ์น์
---
|
| 721 |
+
with st.sidebar:
|
| 722 |
+
st.subheader("๐ ์์คํ
์ ๋ณด")
|
| 723 |
+
st.write(f"๐ ๏ธ MCP ๋๊ตฌ ์: {st.session_state.get('tool_count', '์ด๊ธฐํ ์ค...')}")
|
| 724 |
+
selected_model_name = st.session_state.selected_model
|
| 725 |
+
st.write(f"๐ง ํ์ฌ ๋ชจ๋ธ: {selected_model_name}")
|
| 726 |
+
|
| 727 |
+
# ์ค์ ์ ์ฉํ๊ธฐ ๋ฒํผ์ ์ฌ๊ธฐ๋ก ์ด๋
|
| 728 |
+
if st.button(
|
| 729 |
+
"์ค์ ์ ์ฉํ๊ธฐ",
|
| 730 |
+
key="apply_button",
|
| 731 |
+
type="primary",
|
| 732 |
+
use_container_width=True,
|
| 733 |
+
):
|
| 734 |
+
# ์ ์ฉ ์ค ๋ฉ์์ง ํ์
|
| 735 |
+
apply_status = st.empty()
|
| 736 |
+
with apply_status.container():
|
| 737 |
+
st.warning("๐ ๋ณ๊ฒฝ์ฌํญ์ ์ ์ฉํ๊ณ ์์ต๋๋ค. ์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์...")
|
| 738 |
+
progress_bar = st.progress(0)
|
| 739 |
+
|
| 740 |
+
# ์ค์ ์ ์ฅ
|
| 741 |
+
st.session_state.mcp_config_text = json.dumps(
|
| 742 |
+
st.session_state.pending_mcp_config, indent=2, ensure_ascii=False
|
| 743 |
+
)
|
| 744 |
+
|
| 745 |
+
# config.json ํ์ผ์ ์ค์ ์ ์ฅ
|
| 746 |
+
save_result = save_config_to_json(st.session_state.pending_mcp_config)
|
| 747 |
+
if not save_result:
|
| 748 |
+
st.error("โ ์ค์ ํ์ผ ์ ์ฅ์ ์คํจํ์ต๋๋ค.")
|
| 749 |
+
|
| 750 |
+
progress_bar.progress(15)
|
| 751 |
+
|
| 752 |
+
# ์ธ์
์ด๊ธฐํ ์ค๋น
|
| 753 |
+
st.session_state.session_initialized = False
|
| 754 |
+
st.session_state.agent = None
|
| 755 |
+
|
| 756 |
+
# ์งํ ์ํ ์
๋ฐ์ดํธ
|
| 757 |
+
progress_bar.progress(30)
|
| 758 |
+
|
| 759 |
+
# ์ด๊ธฐํ ์คํ
|
| 760 |
+
success = st.session_state.event_loop.run_until_complete(
|
| 761 |
+
initialize_session(st.session_state.pending_mcp_config)
|
| 762 |
+
)
|
| 763 |
+
|
| 764 |
+
# ์งํ ์ํ ์
๋ฐ์ดํธ
|
| 765 |
+
progress_bar.progress(100)
|
| 766 |
+
|
| 767 |
+
if success:
|
| 768 |
+
st.success("โ
์๋ก์ด ์ค์ ์ด ์ ์ฉ๋์์ต๋๋ค.")
|
| 769 |
+
# ๋๊ตฌ ์ถ๊ฐ expander ์ ๊ธฐ
|
| 770 |
+
if "mcp_tools_expander" in st.session_state:
|
| 771 |
+
st.session_state.mcp_tools_expander = False
|
| 772 |
+
else:
|
| 773 |
+
st.error("โ ์ค์ ์ ์ฉ์ ์คํจํ์์ต๋๋ค.")
|
| 774 |
+
|
| 775 |
+
# ํ์ด์ง ์๋ก๊ณ ์นจ
|
| 776 |
+
st.rerun()
|
| 777 |
+
|
| 778 |
+
st.divider() # ๊ตฌ๋ถ์ ์ถ๊ฐ
|
| 779 |
+
|
| 780 |
+
# ์์
๋ฒํผ ์น์
|
| 781 |
+
st.subheader("๐ ์์
")
|
| 782 |
+
|
| 783 |
+
# ๋ํ ์ด๊ธฐํ ๋ฒํผ
|
| 784 |
+
if st.button("๋ํ ์ด๊ธฐํ", use_container_width=True, type="primary"):
|
| 785 |
+
# thread_id ์ด๊ธฐํ
|
| 786 |
+
st.session_state.thread_id = random_uuid()
|
| 787 |
+
|
| 788 |
+
# ๋ํ ํ์คํ ๋ฆฌ ์ด๊ธฐํ
|
| 789 |
+
st.session_state.history = []
|
| 790 |
+
|
| 791 |
+
# ์๋ฆผ ๋ฉ์์ง
|
| 792 |
+
st.success("โ
๋ํ๊ฐ ์ด๊ธฐํ๋์์ต๋๋ค.")
|
| 793 |
+
|
| 794 |
+
# ํ์ด์ง ์๋ก๊ณ ์นจ
|
| 795 |
+
st.rerun()
|
| 796 |
+
|
| 797 |
+
# ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ด ํ์ฑํ๋ ๊ฒฝ์ฐ์๋ง ๋ก๊ทธ์์ ๋ฒํผ ํ์
|
| 798 |
+
if use_login and st.session_state.authenticated:
|
| 799 |
+
st.divider() # ๊ตฌ๋ถ์ ์ถ๊ฐ
|
| 800 |
+
if st.button("๋ก๊ทธ์์", use_container_width=True, type="secondary"):
|
| 801 |
+
st.session_state.authenticated = False
|
| 802 |
+
st.success("โ
๋ก๊ทธ์์ ๋์์ต๋๋ค.")
|
| 803 |
+
st.rerun()
|
| 804 |
+
|
| 805 |
+
# --- ๊ธฐ๋ณธ ์ธ์
์ด๊ธฐํ (์ด๊ธฐํ๋์ง ์์ ๊ฒฝ์ฐ) ---
|
| 806 |
+
if not st.session_state.session_initialized:
|
| 807 |
+
st.info(
|
| 808 |
+
"MCP ์๋ฒ์ ์์ด์ ํธ๊ฐ ์ด๊ธฐํ๋์ง ์์์ต๋๋ค. ์ผ์ชฝ ์ฌ์ด๋๋ฐ์ '์ค์ ์ ์ฉํ๊ธฐ' ๋ฒํผ์ ํด๋ฆญํ์ฌ ์ด๊ธฐํํด์ฃผ์ธ์."
|
| 809 |
+
)
|
| 810 |
+
|
| 811 |
+
|
| 812 |
+
# --- ๋ํ ๊ธฐ๋ก ์ถ๋ ฅ ---
|
| 813 |
+
print_message()
|
| 814 |
+
|
| 815 |
+
# --- ์ฌ์ฉ์ ์
๋ ฅ ๋ฐ ์ฒ๋ฆฌ ---
|
| 816 |
+
user_query = st.chat_input("๐ฌ ์ง๋ฌธ์ ์
๋ ฅํ์ธ์")
|
| 817 |
+
if user_query:
|
| 818 |
+
if st.session_state.session_initialized:
|
| 819 |
+
st.chat_message("user", avatar="๐งโ๐ป").markdown(user_query)
|
| 820 |
+
with st.chat_message("assistant", avatar="๐ค"):
|
| 821 |
+
tool_placeholder = st.empty()
|
| 822 |
+
text_placeholder = st.empty()
|
| 823 |
+
resp, final_text, final_tool = (
|
| 824 |
+
st.session_state.event_loop.run_until_complete(
|
| 825 |
+
process_query(
|
| 826 |
+
user_query,
|
| 827 |
+
text_placeholder,
|
| 828 |
+
tool_placeholder,
|
| 829 |
+
st.session_state.timeout_seconds,
|
| 830 |
+
)
|
| 831 |
+
)
|
| 832 |
+
)
|
| 833 |
+
if "error" in resp:
|
| 834 |
+
st.error(resp["error"])
|
| 835 |
+
else:
|
| 836 |
+
st.session_state.history.append({"role": "user", "content": user_query})
|
| 837 |
+
st.session_state.history.append(
|
| 838 |
+
{"role": "assistant", "content": final_text}
|
| 839 |
+
)
|
| 840 |
+
if final_tool.strip():
|
| 841 |
+
st.session_state.history.append(
|
| 842 |
+
{"role": "assistant_tool", "content": final_tool}
|
| 843 |
+
)
|
| 844 |
+
st.rerun()
|
| 845 |
+
else:
|
| 846 |
+
st.warning(
|
| 847 |
+
"โ ๏ธ MCP ์๋ฒ์ ์์ด์ ํธ๊ฐ ์ด๊ธฐํ๋์ง ์์์ต๋๋ค. ์ผ์ชฝ ์ฌ์ด๋๋ฐ์ '์ค์ ์ ์ฉํ๊ธฐ' ๋ฒํผ์ ํด๋ฆญํ์ฌ ์ด๊ธฐํํด์ฃผ์ธ์."
|
| 848 |
+
)
|
assets/add-tools.png
ADDED
|
Git LFS Details
|
assets/apply-tool-configuration.png
ADDED
|
Git LFS Details
|
assets/architecture.png
ADDED
|
Git LFS Details
|
assets/check-status.png
ADDED
|
Git LFS Details
|
assets/project-demo.png
ADDED
|
Git LFS Details
|
assets/smithery-copy-json.png
ADDED
|
Git LFS Details
|
assets/smithery-json.png
ADDED
|
Git LFS Details
|
config.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"get_current_time": {
|
| 3 |
+
"command": "python",
|
| 4 |
+
"args": [
|
| 5 |
+
"./mcp_server_time.py"
|
| 6 |
+
],
|
| 7 |
+
"transport": "stdio"
|
| 8 |
+
}
|
| 9 |
+
}
|
dockers/.env.example
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ANTHROPIC_API_KEY=sk-ant-api03...
|
| 2 |
+
OPENAI_API_KEY=sk-proj-o0gulL2J2a...
|
| 3 |
+
LANGSMITH_API_KEY=lsv2_sk_ed22...
|
| 4 |
+
LANGSMITH_TRACING=true
|
| 5 |
+
LANGSMITH_ENDPOINT=https://api.smith.langchain.com
|
| 6 |
+
LANGSMITH_PROJECT=LangGraph-MCP-Agents
|
| 7 |
+
|
| 8 |
+
USE_LOGIN=true
|
| 9 |
+
USER_ID=admin
|
| 10 |
+
USER_PASSWORD=admin1234
|
dockers/config.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"get_current_time": {
|
| 3 |
+
"command": "python",
|
| 4 |
+
"args": [
|
| 5 |
+
"./mcp_server_time.py"
|
| 6 |
+
],
|
| 7 |
+
"transport": "stdio"
|
| 8 |
+
}
|
| 9 |
+
}
|
dockers/docker-compose-KOR-mac.yaml
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
app:
|
| 3 |
+
build:
|
| 4 |
+
context: .
|
| 5 |
+
dockerfile: Dockerfile
|
| 6 |
+
args:
|
| 7 |
+
BUILDPLATFORM: ${BUILDPLATFORM:-linux/arm64}
|
| 8 |
+
TARGETPLATFORM: "linux/arm64"
|
| 9 |
+
image: teddylee777/langgraph-mcp-agents:KOR-0.2.1
|
| 10 |
+
platform: "linux/arm64"
|
| 11 |
+
ports:
|
| 12 |
+
- "8585:8585"
|
| 13 |
+
env_file:
|
| 14 |
+
- ./.env
|
| 15 |
+
environment:
|
| 16 |
+
- PYTHONUNBUFFERED=1
|
| 17 |
+
# Mac-specific optimizations
|
| 18 |
+
- NODE_OPTIONS=--max_old_space_size=2048
|
| 19 |
+
# Delegated file system performance for macOS
|
| 20 |
+
- PYTHONMALLOC=malloc
|
| 21 |
+
- USE_LOGIN=${USE_LOGIN:-false}
|
| 22 |
+
- USER_ID=${USER_ID:-}
|
| 23 |
+
- USER_PASSWORD=${USER_PASSWORD:-}
|
| 24 |
+
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
| 25 |
+
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
| 26 |
+
- NODE_OPTIONS=${NODE_OPTIONS:-}
|
| 27 |
+
volumes:
|
| 28 |
+
- ./data:/app/data:cached
|
| 29 |
+
- ./config.json:/app/config.json
|
| 30 |
+
restart: unless-stopped
|
| 31 |
+
healthcheck:
|
| 32 |
+
test: ["CMD", "curl", "--fail", "http://localhost:8585/_stcore/health"]
|
| 33 |
+
interval: 30s
|
| 34 |
+
timeout: 10s
|
| 35 |
+
retries: 3
|
| 36 |
+
start_period: 40s
|
dockers/docker-compose-KOR.yaml
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
app:
|
| 3 |
+
build:
|
| 4 |
+
context: .
|
| 5 |
+
dockerfile: Dockerfile
|
| 6 |
+
args:
|
| 7 |
+
BUILDPLATFORM: ${BUILDPLATFORM:-linux/amd64}
|
| 8 |
+
TARGETPLATFORM: ${TARGETPLATFORM:-linux/amd64}
|
| 9 |
+
image: teddylee777/langgraph-mcp-agents:KOR-0.2.1
|
| 10 |
+
platform: ${TARGETPLATFORM:-linux/amd64}
|
| 11 |
+
ports:
|
| 12 |
+
- "8585:8585"
|
| 13 |
+
volumes:
|
| 14 |
+
- ./.env:/app/.env:ro
|
| 15 |
+
- ./data:/app/data:rw
|
| 16 |
+
- ./config.json:/app/config.json
|
| 17 |
+
env_file:
|
| 18 |
+
- ./.env
|
| 19 |
+
environment:
|
| 20 |
+
- PYTHONUNBUFFERED=1
|
| 21 |
+
- USE_LOGIN=${USE_LOGIN:-false}
|
| 22 |
+
- USER_ID=${USER_ID:-}
|
| 23 |
+
- USER_PASSWORD=${USER_PASSWORD:-}
|
| 24 |
+
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
| 25 |
+
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
| 26 |
+
- NODE_OPTIONS=${NODE_OPTIONS:-}
|
| 27 |
+
restart: unless-stopped
|
| 28 |
+
healthcheck:
|
| 29 |
+
test: ["CMD", "curl", "--fail", "http://localhost:8585/_stcore/health"]
|
| 30 |
+
interval: 30s
|
| 31 |
+
timeout: 10s
|
| 32 |
+
retries: 3
|
| 33 |
+
start_period: 40s
|
dockers/docker-compose-mac.yaml
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
app:
|
| 3 |
+
build:
|
| 4 |
+
context: .
|
| 5 |
+
dockerfile: Dockerfile
|
| 6 |
+
args:
|
| 7 |
+
BUILDPLATFORM: ${BUILDPLATFORM:-linux/arm64}
|
| 8 |
+
TARGETPLATFORM: "linux/arm64"
|
| 9 |
+
image: teddylee777/langgraph-mcp-agents:0.2.1
|
| 10 |
+
platform: "linux/arm64"
|
| 11 |
+
ports:
|
| 12 |
+
- "8585:8585"
|
| 13 |
+
env_file:
|
| 14 |
+
- ./.env
|
| 15 |
+
environment:
|
| 16 |
+
- PYTHONUNBUFFERED=1
|
| 17 |
+
# Mac-specific optimizations
|
| 18 |
+
- NODE_OPTIONS=--max_old_space_size=2048
|
| 19 |
+
# Delegated file system performance for macOS
|
| 20 |
+
- PYTHONMALLOC=malloc
|
| 21 |
+
- USE_LOGIN=${USE_LOGIN:-false}
|
| 22 |
+
- USER_ID=${USER_ID:-}
|
| 23 |
+
- USER_PASSWORD=${USER_PASSWORD:-}
|
| 24 |
+
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
| 25 |
+
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
| 26 |
+
- NODE_OPTIONS=${NODE_OPTIONS:-}
|
| 27 |
+
volumes:
|
| 28 |
+
- ./data:/app/data:cached
|
| 29 |
+
- ./config.json:/app/config.json
|
| 30 |
+
restart: unless-stopped
|
| 31 |
+
healthcheck:
|
| 32 |
+
test: ["CMD", "curl", "--fail", "http://localhost:8585/_stcore/health"]
|
| 33 |
+
interval: 30s
|
| 34 |
+
timeout: 10s
|
| 35 |
+
retries: 3
|
| 36 |
+
start_period: 40s
|
dockers/docker-compose.yaml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
app:
|
| 3 |
+
build:
|
| 4 |
+
context: .
|
| 5 |
+
dockerfile: Dockerfile
|
| 6 |
+
args:
|
| 7 |
+
BUILDPLATFORM: ${BUILDPLATFORM:-linux/amd64}
|
| 8 |
+
TARGETPLATFORM: ${TARGETPLATFORM:-linux/amd64}
|
| 9 |
+
image: teddylee777/langgraph-mcp-agents:0.2.1
|
| 10 |
+
platform: ${TARGETPLATFORM:-linux/amd64}
|
| 11 |
+
ports:
|
| 12 |
+
- "8585:8585"
|
| 13 |
+
volumes:
|
| 14 |
+
# Optionally, you can remove this volume if you donโt need the file at runtime
|
| 15 |
+
- ./.env:/app/.env:ro
|
| 16 |
+
- ./data:/app/data:rw
|
| 17 |
+
- ./config.json:/app/config.json
|
| 18 |
+
env_file:
|
| 19 |
+
- ./.env
|
| 20 |
+
environment:
|
| 21 |
+
- PYTHONUNBUFFERED=1
|
| 22 |
+
- USE_LOGIN=${USE_LOGIN:-false}
|
| 23 |
+
- USER_ID=${USER_ID:-}
|
| 24 |
+
- USER_PASSWORD=${USER_PASSWORD:-}
|
| 25 |
+
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
| 26 |
+
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
| 27 |
+
- NODE_OPTIONS=${NODE_OPTIONS:-}
|
| 28 |
+
restart: unless-stopped
|
| 29 |
+
healthcheck:
|
| 30 |
+
test: ["CMD", "curl", "--fail", "http://localhost:8585/_stcore/health"]
|
| 31 |
+
interval: 30s
|
| 32 |
+
timeout: 10s
|
| 33 |
+
retries: 3
|
| 34 |
+
start_period: 40s
|
example_config.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"get_current_time": {
|
| 3 |
+
"command": "python",
|
| 4 |
+
"args": ["./mcp_server_time.py"],
|
| 5 |
+
"transport": "stdio"
|
| 6 |
+
}
|
| 7 |
+
}
|
mcp_server_local.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from mcp.server.fastmcp import FastMCP
|
| 2 |
+
|
| 3 |
+
# Initialize FastMCP server with configuration
|
| 4 |
+
mcp = FastMCP(
|
| 5 |
+
"Weather", # Name of the MCP server
|
| 6 |
+
instructions="You are a weather assistant that can answer questions about the weather in a given location.", # Instructions for the LLM on how to use this tool
|
| 7 |
+
host="0.0.0.0", # Host address (0.0.0.0 allows connections from any IP)
|
| 8 |
+
port=8005, # Port number for the server
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@mcp.tool()
|
| 13 |
+
async def get_weather(location: str) -> str:
|
| 14 |
+
"""
|
| 15 |
+
Get current weather information for the specified location.
|
| 16 |
+
|
| 17 |
+
This function simulates a weather service by returning a fixed response.
|
| 18 |
+
In a production environment, this would connect to a real weather API.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
location (str): The name of the location (city, region, etc.) to get weather for
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
str: A string containing the weather information for the specified location
|
| 25 |
+
"""
|
| 26 |
+
# Return a mock weather response
|
| 27 |
+
# In a real implementation, this would call a weather API
|
| 28 |
+
return f"It's always Sunny in {location}"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
if __name__ == "__main__":
|
| 32 |
+
# Start the MCP server with stdio transport
|
| 33 |
+
# stdio transport allows the server to communicate with clients
|
| 34 |
+
# through standard input/output streams, making it suitable for
|
| 35 |
+
# local development and testing
|
| 36 |
+
mcp.run(transport="stdio")
|
mcp_server_rag.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 2 |
+
from langchain_community.document_loaders import PyMuPDFLoader
|
| 3 |
+
from langchain_community.vectorstores import FAISS
|
| 4 |
+
from langchain_openai import OpenAIEmbeddings
|
| 5 |
+
from mcp.server.fastmcp import FastMCP
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
# Load environment variables from .env file (contains API keys)
|
| 10 |
+
load_dotenv(override=True)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def create_retriever() -> Any:
|
| 14 |
+
"""
|
| 15 |
+
Creates and returns a document retriever based on FAISS vector store.
|
| 16 |
+
|
| 17 |
+
This function performs the following steps:
|
| 18 |
+
1. Loads a PDF document(place your PDF file in the data folder)
|
| 19 |
+
2. Splits the document into manageable chunks
|
| 20 |
+
3. Creates embeddings for each chunk
|
| 21 |
+
4. Builds a FAISS vector store from the embeddings
|
| 22 |
+
5. Returns a retriever interface to the vector store
|
| 23 |
+
|
| 24 |
+
Returns:
|
| 25 |
+
Any: A retriever object that can be used to query the document database
|
| 26 |
+
"""
|
| 27 |
+
# Step 1: Load Documents
|
| 28 |
+
# PyMuPDFLoader is used to extract text from PDF files
|
| 29 |
+
loader = PyMuPDFLoader("data/sample.pdf")
|
| 30 |
+
docs = loader.load()
|
| 31 |
+
|
| 32 |
+
# Step 2: Split Documents
|
| 33 |
+
# Recursive splitter divides documents into chunks with some overlap to maintain context
|
| 34 |
+
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=50)
|
| 35 |
+
split_documents = text_splitter.split_documents(docs)
|
| 36 |
+
|
| 37 |
+
# Step 3: Create Embeddings
|
| 38 |
+
# OpenAI's text-embedding-3-small model is used to convert text chunks into vector embeddings
|
| 39 |
+
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
|
| 40 |
+
|
| 41 |
+
# Step 4: Create Vector Database
|
| 42 |
+
# FAISS is an efficient similarity search library that stores vector embeddings
|
| 43 |
+
# and allows for fast retrieval of similar vectors
|
| 44 |
+
vectorstore = FAISS.from_documents(documents=split_documents, embedding=embeddings)
|
| 45 |
+
|
| 46 |
+
# Step 5: Create Retriever
|
| 47 |
+
# The retriever provides an interface to search the vector database
|
| 48 |
+
# and retrieve documents relevant to a query
|
| 49 |
+
retriever = vectorstore.as_retriever()
|
| 50 |
+
return retriever
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# Initialize FastMCP server with configuration
|
| 54 |
+
mcp = FastMCP(
|
| 55 |
+
"Retriever",
|
| 56 |
+
instructions="A Retriever that can retrieve information from the database.",
|
| 57 |
+
host="0.0.0.0",
|
| 58 |
+
port=8005,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@mcp.tool()
|
| 63 |
+
async def retrieve(query: str) -> str:
|
| 64 |
+
"""
|
| 65 |
+
Retrieves information from the document database based on the query.
|
| 66 |
+
|
| 67 |
+
This function creates a retriever, queries it with the provided input,
|
| 68 |
+
and returns the concatenated content of all retrieved documents.
|
| 69 |
+
|
| 70 |
+
Args:
|
| 71 |
+
query (str): The search query to find relevant information
|
| 72 |
+
|
| 73 |
+
Returns:
|
| 74 |
+
str: Concatenated text content from all retrieved documents
|
| 75 |
+
"""
|
| 76 |
+
# Create a new retriever instance for each query
|
| 77 |
+
# Note: In production, consider caching the retriever for better performance
|
| 78 |
+
retriever = create_retriever()
|
| 79 |
+
|
| 80 |
+
# Use the invoke() method to get relevant documents based on the query
|
| 81 |
+
retrieved_docs = retriever.invoke(query)
|
| 82 |
+
|
| 83 |
+
# Join all document contents with newlines and return as a single string
|
| 84 |
+
return "\n".join([doc.page_content for doc in retrieved_docs])
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
if __name__ == "__main__":
|
| 88 |
+
# Run the MCP server with stdio transport for integration with MCP clients
|
| 89 |
+
mcp.run(transport="stdio")
|
mcp_server_remote.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from mcp.server.fastmcp import FastMCP
|
| 2 |
+
|
| 3 |
+
mcp = FastMCP(
|
| 4 |
+
"Weather", # Name of the MCP server
|
| 5 |
+
instructions="You are a weather assistant that can answer questions about the weather in a given location.", # Instructions for the LLM on how to use this tool
|
| 6 |
+
host="0.0.0.0", # Host address (0.0.0.0 allows connections from any IP)
|
| 7 |
+
port=8005, # Port number for the server
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@mcp.tool()
|
| 12 |
+
async def get_weather(location: str) -> str:
|
| 13 |
+
"""
|
| 14 |
+
Get current weather information for the specified location.
|
| 15 |
+
|
| 16 |
+
This function simulates a weather service by returning a fixed response.
|
| 17 |
+
In a production environment, this would connect to a real weather API.
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
location (str): The name of the location (city, region, etc.) to get weather for
|
| 21 |
+
|
| 22 |
+
Returns:
|
| 23 |
+
str: A string containing the weather information for the specified location
|
| 24 |
+
"""
|
| 25 |
+
# Return a mock weather response
|
| 26 |
+
# In a real implementation, this would call a weather API
|
| 27 |
+
return f"It's always Sunny in {location}"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
if __name__ == "__main__":
|
| 31 |
+
# Print a message indicating the server is starting
|
| 32 |
+
print("mcp remote server is running...")
|
| 33 |
+
|
| 34 |
+
# Start the MCP server with SSE transport
|
| 35 |
+
# Server-Sent Events (SSE) transport allows the server to communicate with clients
|
| 36 |
+
# over HTTP, making it suitable for remote/distributed deployments
|
| 37 |
+
mcp.run(transport="sse")
|
mcp_server_time.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from mcp.server.fastmcp import FastMCP
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
import pytz
|
| 4 |
+
from typing import Optional
|
| 5 |
+
|
| 6 |
+
# Initialize FastMCP server with configuration
|
| 7 |
+
mcp = FastMCP(
|
| 8 |
+
"TimeService", # Name of the MCP server
|
| 9 |
+
instructions="You are a time assistant that can provide the current time for different timezones.", # Instructions for the LLM on how to use this tool
|
| 10 |
+
host="0.0.0.0", # Host address (0.0.0.0 allows connections from any IP)
|
| 11 |
+
port=8005, # Port number for the server
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@mcp.tool()
|
| 16 |
+
async def get_current_time(timezone: Optional[str] = "Asia/Seoul") -> str:
|
| 17 |
+
"""
|
| 18 |
+
Get current time information for the specified timezone.
|
| 19 |
+
|
| 20 |
+
This function returns the current system time for the requested timezone.
|
| 21 |
+
|
| 22 |
+
Args:
|
| 23 |
+
timezone (str, optional): The timezone to get current time for. Defaults to "Asia/Seoul".
|
| 24 |
+
|
| 25 |
+
Returns:
|
| 26 |
+
str: A string containing the current time information for the specified timezone
|
| 27 |
+
"""
|
| 28 |
+
try:
|
| 29 |
+
# Get the timezone object
|
| 30 |
+
tz = pytz.timezone(timezone)
|
| 31 |
+
|
| 32 |
+
# Get current time in the specified timezone
|
| 33 |
+
current_time = datetime.now(tz)
|
| 34 |
+
|
| 35 |
+
# Format the time as a string
|
| 36 |
+
formatted_time = current_time.strftime("%Y-%m-%d %H:%M:%S %Z")
|
| 37 |
+
|
| 38 |
+
return f"Current time in {timezone} is: {formatted_time}"
|
| 39 |
+
except pytz.exceptions.UnknownTimeZoneError:
|
| 40 |
+
return f"Error: Unknown timezone '{timezone}'. Please provide a valid timezone."
|
| 41 |
+
except Exception as e:
|
| 42 |
+
return f"Error getting time: {str(e)}"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
if __name__ == "__main__":
|
| 46 |
+
# Start the MCP server with stdio transport
|
| 47 |
+
# stdio transport allows the server to communicate with clients
|
| 48 |
+
# through standard input/output streams, making it suitable for
|
| 49 |
+
# local development and testing
|
| 50 |
+
mcp.run(transport="stdio")
|
packages.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
curl
|
| 2 |
+
gnupg
|
| 3 |
+
ca-certificates
|
| 4 |
+
nodejs
|
| 5 |
+
npm
|
pyproject.toml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "langgraph-mcp-agents"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "LangGraph Agent with MCP Adapters"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.12"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"nest-asyncio>=1.6.0",
|
| 9 |
+
"faiss-cpu>=1.10.0",
|
| 10 |
+
"jupyter>=1.1.1",
|
| 11 |
+
"langchain-anthropic>=0.3.10",
|
| 12 |
+
"langchain-community>=0.3.20",
|
| 13 |
+
"langchain-mcp-adapters>=0.0.7",
|
| 14 |
+
"langchain-openai>=0.3.11",
|
| 15 |
+
"langgraph>=0.3.21",
|
| 16 |
+
"mcp[cli]>=1.6.0",
|
| 17 |
+
"notebook>=7.3.3",
|
| 18 |
+
"pymupdf>=1.25.4",
|
| 19 |
+
"python-dotenv>=1.1.0",
|
| 20 |
+
"streamlit>=1.44.1",
|
| 21 |
+
]
|
requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
faiss-cpu>=1.10.0
|
| 2 |
+
jupyter>=1.1.1
|
| 3 |
+
langchain-anthropic>=0.3.10
|
| 4 |
+
langchain-community>=0.3.20
|
| 5 |
+
langchain-mcp-adapters>=0.0.7
|
| 6 |
+
langchain-openai>=0.3.11
|
| 7 |
+
langgraph>=0.3.21
|
| 8 |
+
mcp>=1.6.0
|
| 9 |
+
notebook>=7.3.3
|
| 10 |
+
pymupdf>=1.25.4
|
| 11 |
+
python-dotenv>=1.1.0
|
| 12 |
+
streamlit>=1.44.1
|
| 13 |
+
nest-asyncio>=1.6.0
|
utils.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict, List, Callable, Optional
|
| 2 |
+
from langchain_core.messages import BaseMessage
|
| 3 |
+
from langchain_core.runnables import RunnableConfig
|
| 4 |
+
from langgraph.graph.state import CompiledStateGraph
|
| 5 |
+
import uuid
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def random_uuid():
|
| 9 |
+
return str(uuid.uuid4())
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
async def astream_graph(
|
| 13 |
+
graph: CompiledStateGraph,
|
| 14 |
+
inputs: dict,
|
| 15 |
+
config: Optional[RunnableConfig] = None,
|
| 16 |
+
node_names: List[str] = [],
|
| 17 |
+
callback: Optional[Callable] = None,
|
| 18 |
+
stream_mode: str = "messages",
|
| 19 |
+
include_subgraphs: bool = False,
|
| 20 |
+
) -> Dict[str, Any]:
|
| 21 |
+
"""
|
| 22 |
+
LangGraph์ ์คํ ๊ฒฐ๊ณผ๋ฅผ ๋น๋๊ธฐ์ ์ผ๋ก ์คํธ๋ฆฌ๋ฐํ๊ณ ์ง์ ์ถ๋ ฅํ๋ ํจ์์
๋๋ค.
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
graph (CompiledStateGraph): ์คํํ ์ปดํ์ผ๋ LangGraph ๊ฐ์ฒด
|
| 26 |
+
inputs (dict): ๊ทธ๋ํ์ ์ ๋ฌํ ์
๋ ฅ๊ฐ ๋์
๋๋ฆฌ
|
| 27 |
+
config (Optional[RunnableConfig]): ์คํ ์ค์ (์ ํ์ )
|
| 28 |
+
node_names (List[str], optional): ์ถ๋ ฅํ ๋
ธ๋ ์ด๋ฆ ๋ชฉ๋ก. ๊ธฐ๋ณธ๊ฐ์ ๋น ๋ฆฌ์คํธ
|
| 29 |
+
callback (Optional[Callable], optional): ๊ฐ ์ฒญํฌ ์ฒ๋ฆฌ๋ฅผ ์ํ ์ฝ๋ฐฑ ํจ์. ๊ธฐ๋ณธ๊ฐ์ None
|
| 30 |
+
์ฝ๋ฐฑ ํจ์๋ {"node": str, "content": Any} ํํ์ ๋์
๋๋ฆฌ๋ฅผ ์ธ์๋ก ๋ฐ์ต๋๋ค.
|
| 31 |
+
stream_mode (str, optional): ์คํธ๋ฆฌ๋ฐ ๋ชจ๋ ("messages" ๋๋ "updates"). ๊ธฐ๋ณธ๊ฐ์ "messages"
|
| 32 |
+
include_subgraphs (bool, optional): ์๋ธ๊ทธ๋ํ ํฌํจ ์ฌ๋ถ. ๊ธฐ๋ณธ๊ฐ์ False
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
Dict[str, Any]: ์ต์ข
๊ฒฐ๊ณผ (์ ํ์ )
|
| 36 |
+
"""
|
| 37 |
+
config = config or {}
|
| 38 |
+
final_result = {}
|
| 39 |
+
|
| 40 |
+
def format_namespace(namespace):
|
| 41 |
+
return namespace[-1].split(":")[0] if len(namespace) > 0 else "root graph"
|
| 42 |
+
|
| 43 |
+
prev_node = ""
|
| 44 |
+
|
| 45 |
+
if stream_mode == "messages":
|
| 46 |
+
async for chunk_msg, metadata in graph.astream(
|
| 47 |
+
inputs, config, stream_mode=stream_mode
|
| 48 |
+
):
|
| 49 |
+
curr_node = metadata["langgraph_node"]
|
| 50 |
+
final_result = {
|
| 51 |
+
"node": curr_node,
|
| 52 |
+
"content": chunk_msg,
|
| 53 |
+
"metadata": metadata,
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
# node_names๊ฐ ๋น์ด์๊ฑฐ๋ ํ์ฌ ๋
ธ๋๊ฐ node_names์ ์๋ ๊ฒฝ์ฐ์๋ง ์ฒ๋ฆฌ
|
| 57 |
+
if not node_names or curr_node in node_names:
|
| 58 |
+
# ์ฝ๋ฐฑ ํจ์๊ฐ ์๋ ๊ฒฝ์ฐ ์คํ
|
| 59 |
+
if callback:
|
| 60 |
+
result = callback({"node": curr_node, "content": chunk_msg})
|
| 61 |
+
if hasattr(result, "__await__"):
|
| 62 |
+
await result
|
| 63 |
+
# ์ฝ๋ฐฑ์ด ์๋ ๊ฒฝ์ฐ ๊ธฐ๋ณธ ์ถ๋ ฅ
|
| 64 |
+
else:
|
| 65 |
+
# ๋
ธ๋๊ฐ ๋ณ๊ฒฝ๋ ๊ฒฝ์ฐ์๋ง ๊ตฌ๋ถ์ ์ถ๋ ฅ
|
| 66 |
+
if curr_node != prev_node:
|
| 67 |
+
print("\n" + "=" * 50)
|
| 68 |
+
print(f"๐ Node: \033[1;36m{curr_node}\033[0m ๐")
|
| 69 |
+
print("- " * 25)
|
| 70 |
+
|
| 71 |
+
# Claude/Anthropic ๋ชจ๋ธ์ ํ ํฐ ์ฒญํฌ ์ฒ๋ฆฌ - ํญ์ ํ
์คํธ๋ง ์ถ์ถ
|
| 72 |
+
if hasattr(chunk_msg, "content"):
|
| 73 |
+
# ๋ฆฌ์คํธ ํํ์ content (Anthropic/Claude ์คํ์ผ)
|
| 74 |
+
if isinstance(chunk_msg.content, list):
|
| 75 |
+
for item in chunk_msg.content:
|
| 76 |
+
if isinstance(item, dict) and "text" in item:
|
| 77 |
+
print(item["text"], end="", flush=True)
|
| 78 |
+
# ๋ฌธ์์ด ํํ์ content
|
| 79 |
+
elif isinstance(chunk_msg.content, str):
|
| 80 |
+
print(chunk_msg.content, end="", flush=True)
|
| 81 |
+
# ๊ทธ ์ธ ํํ์ chunk_msg ์ฒ๋ฆฌ
|
| 82 |
+
else:
|
| 83 |
+
print(chunk_msg, end="", flush=True)
|
| 84 |
+
|
| 85 |
+
prev_node = curr_node
|
| 86 |
+
|
| 87 |
+
elif stream_mode == "updates":
|
| 88 |
+
# ์๋ฌ ์์ : ์ธํจํน ๋ฐฉ์ ๋ณ๊ฒฝ
|
| 89 |
+
# REACT ์์ด์ ํธ ๋ฑ ์ผ๋ถ ๊ทธ๋ํ์์๋ ๋จ์ผ ๋์
๋๋ฆฌ๋ง ๋ฐํํจ
|
| 90 |
+
async for chunk in graph.astream(
|
| 91 |
+
inputs, config, stream_mode=stream_mode, subgraphs=include_subgraphs
|
| 92 |
+
):
|
| 93 |
+
# ๋ฐํ ํ์์ ๋ฐ๋ผ ์ฒ๋ฆฌ ๋ฐฉ๋ฒ ๋ถ๊ธฐ
|
| 94 |
+
if isinstance(chunk, tuple) and len(chunk) == 2:
|
| 95 |
+
# ๊ธฐ์กด ์์ ํ์: (namespace, chunk_dict)
|
| 96 |
+
namespace, node_chunks = chunk
|
| 97 |
+
else:
|
| 98 |
+
# ๋จ์ผ ๋์
๋๋ฆฌ๋ง ๋ฐํํ๋ ๊ฒฝ์ฐ (REACT ์์ด์ ํธ ๋ฑ)
|
| 99 |
+
namespace = [] # ๋น ๋ค์์คํ์ด์ค (๋ฃจํธ ๊ทธ๋ํ)
|
| 100 |
+
node_chunks = chunk # chunk ์์ฒด๊ฐ ๋
ธ๋ ์ฒญํฌ ๋์
๋๋ฆฌ
|
| 101 |
+
|
| 102 |
+
# ๋์
๋๋ฆฌ์ธ์ง ํ์ธํ๊ณ ํญ๋ชฉ ์ฒ๋ฆฌ
|
| 103 |
+
if isinstance(node_chunks, dict):
|
| 104 |
+
for node_name, node_chunk in node_chunks.items():
|
| 105 |
+
final_result = {
|
| 106 |
+
"node": node_name,
|
| 107 |
+
"content": node_chunk,
|
| 108 |
+
"namespace": namespace,
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
# node_names๊ฐ ๋น์ด์์ง ์์ ๊ฒฝ์ฐ์๋ง ํํฐ๋ง
|
| 112 |
+
if len(node_names) > 0 and node_name not in node_names:
|
| 113 |
+
continue
|
| 114 |
+
|
| 115 |
+
# ์ฝ๋ฐฑ ํจ์๊ฐ ์๋ ๊ฒฝ์ฐ ์คํ
|
| 116 |
+
if callback is not None:
|
| 117 |
+
result = callback({"node": node_name, "content": node_chunk})
|
| 118 |
+
if hasattr(result, "__await__"):
|
| 119 |
+
await result
|
| 120 |
+
# ์ฝ๋ฐฑ์ด ์๋ ๊ฒฝ์ฐ ๊ธฐ๋ณธ ์ถ๋ ฅ
|
| 121 |
+
else:
|
| 122 |
+
# ๋
ธ๋๊ฐ ๋ณ๊ฒฝ๋ ๊ฒฝ์ฐ์๋ง ๊ตฌ๋ถ์ ์ถ๋ ฅ (messages ๋ชจ๋์ ๋์ผํ๊ฒ)
|
| 123 |
+
if node_name != prev_node:
|
| 124 |
+
print("\n" + "=" * 50)
|
| 125 |
+
print(f"๐ Node: \033[1;36m{node_name}\033[0m ๐")
|
| 126 |
+
print("- " * 25)
|
| 127 |
+
|
| 128 |
+
# ๋
ธ๋์ ์ฒญํฌ ๋ฐ์ดํฐ ์ถ๋ ฅ - ํ
์คํธ ์ค์ฌ์ผ๋ก ์ฒ๋ฆฌ
|
| 129 |
+
if isinstance(node_chunk, dict):
|
| 130 |
+
for k, v in node_chunk.items():
|
| 131 |
+
if isinstance(v, BaseMessage):
|
| 132 |
+
# BaseMessage์ content ์์ฑ์ด ํ
์คํธ๋ ๋ฆฌ์คํธ์ธ ๊ฒฝ์ฐ๋ฅผ ์ฒ๋ฆฌ
|
| 133 |
+
if hasattr(v, "content"):
|
| 134 |
+
if isinstance(v.content, list):
|
| 135 |
+
for item in v.content:
|
| 136 |
+
if (
|
| 137 |
+
isinstance(item, dict)
|
| 138 |
+
and "text" in item
|
| 139 |
+
):
|
| 140 |
+
print(
|
| 141 |
+
item["text"], end="", flush=True
|
| 142 |
+
)
|
| 143 |
+
else:
|
| 144 |
+
print(v.content, end="", flush=True)
|
| 145 |
+
else:
|
| 146 |
+
v.pretty_print()
|
| 147 |
+
elif isinstance(v, list):
|
| 148 |
+
for list_item in v:
|
| 149 |
+
if isinstance(list_item, BaseMessage):
|
| 150 |
+
if hasattr(list_item, "content"):
|
| 151 |
+
if isinstance(list_item.content, list):
|
| 152 |
+
for item in list_item.content:
|
| 153 |
+
if (
|
| 154 |
+
isinstance(item, dict)
|
| 155 |
+
and "text" in item
|
| 156 |
+
):
|
| 157 |
+
print(
|
| 158 |
+
item["text"],
|
| 159 |
+
end="",
|
| 160 |
+
flush=True,
|
| 161 |
+
)
|
| 162 |
+
else:
|
| 163 |
+
print(
|
| 164 |
+
list_item.content,
|
| 165 |
+
end="",
|
| 166 |
+
flush=True,
|
| 167 |
+
)
|
| 168 |
+
else:
|
| 169 |
+
list_item.pretty_print()
|
| 170 |
+
elif (
|
| 171 |
+
isinstance(list_item, dict)
|
| 172 |
+
and "text" in list_item
|
| 173 |
+
):
|
| 174 |
+
print(list_item["text"], end="", flush=True)
|
| 175 |
+
else:
|
| 176 |
+
print(list_item, end="", flush=True)
|
| 177 |
+
elif isinstance(v, dict) and "text" in v:
|
| 178 |
+
print(v["text"], end="", flush=True)
|
| 179 |
+
else:
|
| 180 |
+
print(v, end="", flush=True)
|
| 181 |
+
elif node_chunk is not None:
|
| 182 |
+
if hasattr(node_chunk, "__iter__") and not isinstance(
|
| 183 |
+
node_chunk, str
|
| 184 |
+
):
|
| 185 |
+
for item in node_chunk:
|
| 186 |
+
if isinstance(item, dict) and "text" in item:
|
| 187 |
+
print(item["text"], end="", flush=True)
|
| 188 |
+
else:
|
| 189 |
+
print(item, end="", flush=True)
|
| 190 |
+
else:
|
| 191 |
+
print(node_chunk, end="", flush=True)
|
| 192 |
+
|
| 193 |
+
# ๊ตฌ๋ถ์ ์ ์ฌ๊ธฐ์ ์ถ๋ ฅํ์ง ์์ (messages ๋ชจ๋์ ๋์ผํ๊ฒ)
|
| 194 |
+
|
| 195 |
+
prev_node = node_name
|
| 196 |
+
else:
|
| 197 |
+
# ๋์
๋๋ฆฌ๊ฐ ์๋ ๊ฒฝ์ฐ ์ ์ฒด ์ฒญํฌ ์ถ๋ ฅ
|
| 198 |
+
print("\n" + "=" * 50)
|
| 199 |
+
print(f"๐ Raw output ๐")
|
| 200 |
+
print("- " * 25)
|
| 201 |
+
print(node_chunks, end="", flush=True)
|
| 202 |
+
# ๊ตฌ๋ถ์ ์ ์ฌ๊ธฐ์ ์ถ๋ ฅํ์ง ์์
|
| 203 |
+
final_result = {"content": node_chunks}
|
| 204 |
+
|
| 205 |
+
else:
|
| 206 |
+
raise ValueError(
|
| 207 |
+
f"Invalid stream_mode: {stream_mode}. Must be 'messages' or 'updates'."
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
# ํ์์ ๋ฐ๋ผ ์ต์ข
๊ฒฐ๊ณผ ๋ฐํ
|
| 211 |
+
return final_result
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
async def ainvoke_graph(
|
| 215 |
+
graph: CompiledStateGraph,
|
| 216 |
+
inputs: dict,
|
| 217 |
+
config: Optional[RunnableConfig] = None,
|
| 218 |
+
node_names: List[str] = [],
|
| 219 |
+
callback: Optional[Callable] = None,
|
| 220 |
+
include_subgraphs: bool = True,
|
| 221 |
+
) -> Dict[str, Any]:
|
| 222 |
+
"""
|
| 223 |
+
LangGraph ์ฑ์ ์คํ ๊ฒฐ๊ณผ๋ฅผ ๋น๋๊ธฐ์ ์ผ๋ก ์คํธ๋ฆฌ๋ฐํ์ฌ ์ถ๋ ฅํ๋ ํจ์์
๋๋ค.
|
| 224 |
+
|
| 225 |
+
Args:
|
| 226 |
+
graph (CompiledStateGraph): ์คํํ ์ปดํ์ผ๋ LangGraph ๊ฐ์ฒด
|
| 227 |
+
inputs (dict): ๊ทธ๋ํ์ ์ ๋ฌํ ์
๋ ฅ๊ฐ ๋์
๋๋ฆฌ
|
| 228 |
+
config (Optional[RunnableConfig]): ์คํ ์ค์ (์ ํ์ )
|
| 229 |
+
node_names (List[str], optional): ์ถ๋ ฅํ ๋
ธ๋ ์ด๋ฆ ๋ชฉ๋ก. ๊ธฐ๋ณธ๊ฐ์ ๋น ๋ฆฌ์คํธ
|
| 230 |
+
callback (Optional[Callable], optional): ๊ฐ ์ฒญํฌ ์ฒ๋ฆฌ๋ฅผ ์ํ ์ฝ๋ฐฑ ํจ์. ๊ธฐ๋ณธ๊ฐ์ None
|
| 231 |
+
์ฝ๋ฐฑ ํจ์๋ {"node": str, "content": Any} ํํ์ ๋์
๋๋ฆฌ๋ฅผ ์ธ์๋ก ๋ฐ์ต๋๋ค.
|
| 232 |
+
include_subgraphs (bool, optional): ์๋ธ๊ทธ๋ํ ํฌํจ ์ฌ๋ถ. ๊ธฐ๋ณธ๊ฐ์ True
|
| 233 |
+
|
| 234 |
+
Returns:
|
| 235 |
+
Dict[str, Any]: ์ต์ข
๊ฒฐ๊ณผ (๋ง์ง๋ง ๋
ธ๋์ ์ถ๋ ฅ)
|
| 236 |
+
"""
|
| 237 |
+
config = config or {}
|
| 238 |
+
final_result = {}
|
| 239 |
+
|
| 240 |
+
def format_namespace(namespace):
|
| 241 |
+
return namespace[-1].split(":")[0] if len(namespace) > 0 else "root graph"
|
| 242 |
+
|
| 243 |
+
# subgraphs ๋งค๊ฐ๋ณ์๋ฅผ ํตํด ์๋ธ๊ทธ๋ํ์ ์ถ๋ ฅ๋ ํฌํจ
|
| 244 |
+
async for chunk in graph.astream(
|
| 245 |
+
inputs, config, stream_mode="updates", subgraphs=include_subgraphs
|
| 246 |
+
):
|
| 247 |
+
# ๋ฐํ ํ์์ ๋ฐ๋ผ ์ฒ๋ฆฌ ๋ฐฉ๋ฒ ๋ถ๊ธฐ
|
| 248 |
+
if isinstance(chunk, tuple) and len(chunk) == 2:
|
| 249 |
+
# ๊ธฐ์กด ์์ ํ์: (namespace, chunk_dict)
|
| 250 |
+
namespace, node_chunks = chunk
|
| 251 |
+
else:
|
| 252 |
+
# ๋จ์ผ ๋์
๋๋ฆฌ๋ง ๋ฐํํ๋ ๊ฒฝ์ฐ (REACT ์์ด์ ํธ ๋ฑ)
|
| 253 |
+
namespace = [] # ๋น ๋ค์์คํ์ด์ค (๋ฃจํธ ๊ทธ๋ํ)
|
| 254 |
+
node_chunks = chunk # chunk ์์ฒด๊ฐ ๋
ธ๋ ์ฒญํฌ ๋์
๋๋ฆฌ
|
| 255 |
+
|
| 256 |
+
# ๋์
๋๋ฆฌ์ธ์ง ํ์ธํ๊ณ ํญ๋ชฉ ์ฒ๋ฆฌ
|
| 257 |
+
if isinstance(node_chunks, dict):
|
| 258 |
+
for node_name, node_chunk in node_chunks.items():
|
| 259 |
+
final_result = {
|
| 260 |
+
"node": node_name,
|
| 261 |
+
"content": node_chunk,
|
| 262 |
+
"namespace": namespace,
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
# node_names๊ฐ ๋น์ด์์ง ์์ ๊ฒฝ์ฐ์๋ง ํํฐ๋ง
|
| 266 |
+
if node_names and node_name not in node_names:
|
| 267 |
+
continue
|
| 268 |
+
|
| 269 |
+
# ์ฝ๋ฐฑ ํจ์๊ฐ ์๋ ๊ฒฝ์ฐ ์คํ
|
| 270 |
+
if callback is not None:
|
| 271 |
+
result = callback({"node": node_name, "content": node_chunk})
|
| 272 |
+
# ์ฝ๋ฃจํด์ธ ๊ฒฝ์ฐ await
|
| 273 |
+
if hasattr(result, "__await__"):
|
| 274 |
+
await result
|
| 275 |
+
# ์ฝ๋ฐฑ์ด ์๋ ๊ฒฝ์ฐ ๊ธฐ๋ณธ ์ถ๋ ฅ
|
| 276 |
+
else:
|
| 277 |
+
print("\n" + "=" * 50)
|
| 278 |
+
formatted_namespace = format_namespace(namespace)
|
| 279 |
+
if formatted_namespace == "root graph":
|
| 280 |
+
print(f"๐ Node: \033[1;36m{node_name}\033[0m ๐")
|
| 281 |
+
else:
|
| 282 |
+
print(
|
| 283 |
+
f"๐ Node: \033[1;36m{node_name}\033[0m in [\033[1;33m{formatted_namespace}\033[0m] ๐"
|
| 284 |
+
)
|
| 285 |
+
print("- " * 25)
|
| 286 |
+
|
| 287 |
+
# ๋
ธ๋์ ์ฒญํฌ ๋ฐ์ดํฐ ์ถ๋ ฅ
|
| 288 |
+
if isinstance(node_chunk, dict):
|
| 289 |
+
for k, v in node_chunk.items():
|
| 290 |
+
if isinstance(v, BaseMessage):
|
| 291 |
+
v.pretty_print()
|
| 292 |
+
elif isinstance(v, list):
|
| 293 |
+
for list_item in v:
|
| 294 |
+
if isinstance(list_item, BaseMessage):
|
| 295 |
+
list_item.pretty_print()
|
| 296 |
+
else:
|
| 297 |
+
print(list_item)
|
| 298 |
+
elif isinstance(v, dict):
|
| 299 |
+
for node_chunk_key, node_chunk_value in v.items():
|
| 300 |
+
print(f"{node_chunk_key}:\n{node_chunk_value}")
|
| 301 |
+
else:
|
| 302 |
+
print(f"\033[1;32m{k}\033[0m:\n{v}")
|
| 303 |
+
elif node_chunk is not None:
|
| 304 |
+
if hasattr(node_chunk, "__iter__") and not isinstance(
|
| 305 |
+
node_chunk, str
|
| 306 |
+
):
|
| 307 |
+
for item in node_chunk:
|
| 308 |
+
print(item)
|
| 309 |
+
else:
|
| 310 |
+
print(node_chunk)
|
| 311 |
+
print("=" * 50)
|
| 312 |
+
else:
|
| 313 |
+
# ๋์
๋๋ฆฌ๊ฐ ์๋ ๊ฒฝ์ฐ ์ ์ฒด ์ฒญํฌ ์ถ๋ ฅ
|
| 314 |
+
print("\n" + "=" * 50)
|
| 315 |
+
print(f"๐ Raw output ๐")
|
| 316 |
+
print("- " * 25)
|
| 317 |
+
print(node_chunks)
|
| 318 |
+
print("=" * 50)
|
| 319 |
+
final_result = {"content": node_chunks}
|
| 320 |
+
|
| 321 |
+
# ์ต์ข
๊ฒฐ๊ณผ ๋ฐํ
|
| 322 |
+
return final_result
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|