jackkuo commited on
Commit
d7500c0
·
1 Parent(s): c3b9f62
.env ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ ANTHROPIC_API_KEY=sk-0nEqu5ChgjT6aFweA12bC37d6f8f485eAd63848e4c57041d
2
+ ANTHROPIC_BASE_URL=https://openai.sohoyo.io
3
+
4
+ # OPENAI_API_KEY=sk-0nEqu5ChgjT6aFweA12bC37d6f8f485eAd63848e4c57041d
5
+ # OPENAI_BASE_URL=https://openai.sohoyo.io/v1
MCP-HandsOn-KOR.ipynb DELETED
@@ -1,701 +0,0 @@
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_KOR.md DELETED
@@ -1,232 +0,0 @@
1
- # LangGraph 에이전트 + MCP
2
-
3
- [![English](https://img.shields.io/badge/Language-English-blue)](README.md) [![Korean](https://img.shields.io/badge/Language-한국어-red)](README_KOR.md)
4
-
5
- [![GitHub](https://img.shields.io/badge/GitHub-langgraph--mcp--agents-black?logo=github)](https://github.com/teddylee777/langgraph-mcp-agents)
6
- [![License](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
7
- [![Python](https://img.shields.io/badge/Python-≥3.12-blue?logo=python&logoColor=white)](https://www.python.org/)
8
- [![Version](https://img.shields.io/badge/Version-0.1.0-orange)](https://github.com/teddylee777/langgraph-mcp-agents)
9
-
10
- ![project demo](./assets/project-demo.png)
11
-
12
- ## 프로젝트 개요
13
-
14
- ![project architecture](./assets/architecture.png)
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
- ![copy from Smithery](./assets/smithery-copy-json.png)
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
- ![check status](./assets/check-status.png)
200
-
201
- 5. 채팅 인터페이스에서 질문을 하여 구성된 MCP 도구를 활용하는 ReAct 에이전트와 상호작용합니다.
202
-
203
- ![project demo](./assets/project-demo.png)
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
- [![Tutorial Video Thumbnail](https://img.youtube.com/vi/ISrYHGg2C2c/maxresdefault.jpg)](https://youtu.be/ISrYHGg2C2c?si=eWmKFVUS1BLtPm5U)
228
-
229
- ## 참고 자료
230
-
231
- - https://github.com/langchain-ai/langchain-mcp-adapters
232
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
__pycache__/app.cpython-310.pyc ADDED
Binary file (21.6 kB). View file
 
__pycache__/app.cpython-312.pyc ADDED
Binary file (38.6 kB). View file
 
__pycache__/utils.cpython-310.pyc ADDED
Binary file (6.32 kB). View file
 
__pycache__/utils.cpython-312.pyc ADDED
Binary file (10.5 kB). View file
 
app.py CHANGED
@@ -52,10 +52,14 @@ def load_config_from_json():
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)
@@ -186,8 +190,8 @@ Guidelines:
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
 
@@ -198,10 +202,10 @@ if "session_initialized" not in st.session_state:
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
 
@@ -230,6 +234,9 @@ async def cleanup_mcp_client():
230
  # st.warning(traceback.format_exc())
231
 
232
 
 
 
 
233
  def print_message():
234
  """
235
  Displays chat history on the screen.
@@ -272,6 +279,7 @@ def get_streaming_callback(text_placeholder, tool_placeholder):
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
@@ -288,9 +296,26 @@ def get_streaming_callback(text_placeholder, tool_placeholder):
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]
@@ -320,12 +345,16 @@ def get_streaming_callback(text_placeholder, tool_placeholder):
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
@@ -345,9 +374,22 @@ def get_streaming_callback(text_placeholder, tool_placeholder):
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
  ):
@@ -359,17 +401,330 @@ def get_streaming_callback(text_placeholder, tool_placeholder):
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
@@ -412,8 +767,37 @@ async def process_query(query, text_placeholder, tool_placeholder, timeout_secon
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)
@@ -448,6 +832,7 @@ async def initialize_session(mcp_config=None):
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()
@@ -458,7 +843,7 @@ async def initialize_session(mcp_config=None):
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
  ]:
@@ -469,6 +854,7 @@ async def initialize_session(mcp_config=None):
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"],
@@ -497,7 +883,7 @@ with st.sidebar:
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
  ]
@@ -514,7 +900,7 @@ with st.sidebar:
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
@@ -542,7 +928,7 @@ with st.sidebar:
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.",
@@ -655,6 +1041,7 @@ with st.sidebar:
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"
 
52
  }
53
  }
54
 
55
+
56
+
57
  try:
58
  if os.path.exists(CONFIG_FILE_PATH):
59
  with open(CONFIG_FILE_PATH, "r", encoding="utf-8") as f:
60
+ config = json.load(f)
61
+
62
+ return config
63
  else:
64
  # Create file with default settings if it doesn't exist
65
  save_config_to_json(default_config)
 
190
  OUTPUT_TOKEN_INFO = {
191
  "claude-3-5-sonnet-latest": {"max_tokens": 8192},
192
  "claude-3-5-haiku-latest": {"max_tokens": 8192},
193
+ "claude-3-5-sonnet-20241022": {"max_tokens": 64000},
194
+ "gpt-4o": {"max_tokens": 4096}, # 16000},
195
  "gpt-4o-mini": {"max_tokens": 16000},
196
  }
197
 
 
202
  st.session_state.history = [] # List for storing conversation history
203
  st.session_state.mcp_client = None # Storage for MCP client object
204
  st.session_state.timeout_seconds = (
205
+ 30000 # Response generation time limit (seconds), default 120 seconds
206
  )
207
  st.session_state.selected_model = (
208
+ "claude-3-5-sonnet-20241022" # Default model selection
209
  )
210
  st.session_state.recursion_limit = 100 # Recursion call limit, default 100
211
 
 
234
  # st.warning(traceback.format_exc())
235
 
236
 
237
+
238
+
239
+
240
  def print_message():
241
  """
242
  Displays chat history on the screen.
 
279
 
280
  This function creates a callback function to display responses generated from the LLM in real-time.
281
  It displays text responses and tool call information in separate areas.
282
+ It also supports real-time streaming updates from MCP tools.
283
 
284
  Args:
285
  text_placeholder: Streamlit component to display text responses
 
296
  def callback_func(message: dict):
297
  nonlocal accumulated_text, accumulated_tool
298
  message_content = message.get("content", None)
299
+
300
+ # Initialize data counter for tracking data: messages
301
+ if not hasattr(callback_func, '_data_counter'):
302
+ callback_func._data_counter = 0
303
+
304
+ # Initialize persistent storage for all processed data
305
+ if not hasattr(callback_func, '_persistent_data'):
306
+ callback_func._persistent_data = []
307
+ callback_func._persistent_data.append("🚀 **Session Started** - All data will be preserved\n")
308
+ callback_func._persistent_data.append("---\n")
309
+
310
+
311
+
312
+
313
 
314
  if isinstance(message_content, AIMessageChunk):
315
  content = message_content.content
316
+
317
+
318
+
319
  # If content is in list form (mainly occurs in Claude models)
320
  if isinstance(content, list) and len(content) > 0:
321
  message_chunk = content[0]
 
345
  ):
346
  tool_call_info = message_content.tool_calls[0]
347
  accumulated_tool.append("\n```json\n" + str(tool_call_info) + "\n```\n")
348
+
349
+
350
+
351
  with tool_placeholder.expander(
352
  "🔧 Tool Call Information", expanded=True
353
  ):
354
  st.markdown("".join(accumulated_tool))
355
  # Process if content is a simple string
356
  elif isinstance(content, str):
357
+ # Regular text content
358
  accumulated_text.append(content)
359
  text_placeholder.markdown("".join(accumulated_text))
360
  # Process if invalid tool call information exists
 
374
  and message_content.tool_call_chunks
375
  ):
376
  tool_call_chunk = message_content.tool_call_chunks[0]
377
+ tool_name = tool_call_chunk.get('name', 'Unknown')
378
+
379
+ # Only show tool call info if it's a new tool or has meaningful changes
380
+ if not hasattr(callback_func, '_last_tool_name') or callback_func._last_tool_name != tool_name:
381
+ accumulated_tool.append(
382
+ f"\n🔧 **Tool Call**: {tool_name}\n"
383
+ )
384
+ callback_func._last_tool_name = tool_name
385
+
386
+ # Show tool call details in a more compact format
387
  accumulated_tool.append(
388
+ f"```json\n{str(tool_call_chunk)}\n```\n"
389
  )
390
+
391
+
392
+
393
  with tool_placeholder.expander(
394
  "🔧 Tool Call Information", expanded=True
395
  ):
 
401
  ):
402
  tool_call_info = message_content.additional_kwargs["tool_calls"][0]
403
  accumulated_tool.append("\n```json\n" + str(tool_call_info) + "\n```\n")
404
+
405
+
406
+
407
  with tool_placeholder.expander(
408
  "🔧 Tool Call Information", expanded=True
409
  ):
410
  st.markdown("".join(accumulated_tool))
411
  # Process if it's a tool message (tool response)
412
  elif isinstance(message_content, ToolMessage):
413
+ # Don't show Tool Completed immediately - wait for all streaming content
414
+ # Just store the tool name for later display
415
+ if not hasattr(callback_func, '_pending_tool_completion'):
416
+ callback_func._pending_tool_completion = []
417
+ callback_func._pending_tool_completion.append(message_content.name or "Unknown Tool")
418
+
419
+ # Convert streaming text to final result
420
+ streaming_text_items = [item for item in accumulated_tool if item.startswith("\n📊 **Streaming Text**:")]
421
+ if streaming_text_items:
422
+ # Get the last streaming text (most complete)
423
+ last_streaming = streaming_text_items[-1]
424
+ # Extract the text content
425
+ final_text = last_streaming.replace("\n📊 **Streaming Text**: ", "").strip()
426
+ if final_text:
427
+ # Remove all streaming text entries
428
+ accumulated_tool = [item for item in accumulated_tool if not item.startswith("\n📊 **Streaming Text**:")]
429
+ # Add the final complete result
430
+ accumulated_tool.append(f"\n📊 **Final Result**: {final_text}\n")
431
+
432
+ # Handle tool response content
433
+ tool_content = message_content.content
434
+
435
+
436
+ # Handle tool response content
437
+ if isinstance(tool_content, str):
438
+ # Look for SSE data patterns
439
+ if "data:" in tool_content:
440
+ # Parse SSE data and extract meaningful content
441
+ lines = tool_content.split('\n')
442
+ for line in lines:
443
+ line = line.strip()
444
+ if line.startswith('data:'):
445
+ # Increment data counter for each data: message
446
+ callback_func._data_counter += 1
447
+
448
+ try:
449
+ # Extract JSON content from SSE data
450
+ json_str = line[5:].strip() # Remove 'data:' prefix
451
+ if json_str:
452
+ # Try to parse as JSON
453
+ import json
454
+ try:
455
+ data_obj = json.loads(json_str)
456
+ if isinstance(data_obj, dict):
457
+ # Handle different types of SSE data
458
+ if data_obj.get("type") == "result":
459
+ content = data_obj.get("content", "")
460
+ if content:
461
+ # Check for specific server output formats
462
+ if "```bdd-long-task-start" in content:
463
+ # Extract task info
464
+ import re
465
+ match = re.search(r'```bdd-long-task-start\s*\n(.*?)\n```', content, re.DOTALL)
466
+ if match:
467
+ try:
468
+ task_info = json.loads(match.group(1))
469
+ task_id = task_info.get('id', 'Unknown')
470
+ task_label = task_info.get('label', 'Unknown task')
471
+ accumulated_tool.append(f"\n🚀 **Task Started** [{task_id}]: {task_label}\n")
472
+ except:
473
+ accumulated_tool.append(f"\n🚀 **Task Started**: {content}\n")
474
+ # Real-time UI update for task start
475
+ with tool_placeholder.expander("🔧 Tool Call Information", expanded=True):
476
+ # Show data counter at the top
477
+ st.markdown(f"**📊 Total Data Messages: {callback_func._data_counter}**")
478
+ st.markdown("---")
479
+ st.markdown("".join(accumulated_tool))
480
+ elif "```bdd-long-task-end" in content:
481
+ # Extract task info
482
+ import re
483
+ match = re.search(r'```bdd-long-task-end\s*\n(.*?)\n```', content, re.DOTALL)
484
+ if match:
485
+ try:
486
+ task_info = json.loads(match.group(1))
487
+ task_id = task_info.get('id', 'Unknown')
488
+ accumulated_tool.append(f"\n✅ **Task Completed** [{task_id}]\n")
489
+ except:
490
+ accumulated_tool.append(f"\n✅ **Task Completed**: {content}\n")
491
+ # Real-time UI update for task completion
492
+ with tool_placeholder.expander("🔧 Tool Call Information", expanded=True):
493
+ # Show data counter at the top
494
+ st.markdown(f"**📊 Total Data Messages: {callback_func._data_counter}**")
495
+ st.markdown("---")
496
+ st.markdown("".join(accumulated_tool))
497
+ elif "```bdd-resource-lookup" in content:
498
+ # Extract resource info
499
+ import re
500
+ match = re.search(r'```bdd-resource-lookup\s*\n(.*?)\n```', content, re.DOTALL)
501
+ if match:
502
+ try:
503
+ resources = json.loads(match.group(1))
504
+ if isinstance(resources, list):
505
+ accumulated_tool.append(f"\n📚 **Resources Found**: {len(resources)} items\n")
506
+ for i, resource in enumerate(resources[:3]): # Show first 3
507
+ source = resource.get('source', 'Unknown')
508
+ doc_id = resource.get('docId', 'Unknown')
509
+ citation = resource.get('citation', '')
510
+ accumulated_tool.append(f" - {source}: {doc_id} [citation:{citation}]\n")
511
+ if len(resources) > 3:
512
+ accumulated_tool.append(f" ... and {len(resources) - 3} more\n")
513
+ except:
514
+ accumulated_tool.append(f"\n📚 **Resources**: {content}\n")
515
+ # Real-time UI update for resources
516
+ with tool_placeholder.expander("🔧 Tool Call Information", expanded=True):
517
+ # Show data counter at the top
518
+ st.markdown(f"**📊 Total Data Messages: {callback_func._data_counter}**")
519
+ st.markdown("---")
520
+ st.markdown("".join(accumulated_tool))
521
+ elif "```bdd-chat-agent-task" in content:
522
+ # Extract chat agent task info
523
+ import re
524
+ match = re.search(r'```bdd-chat-agent-task\s*\n(.*?)\n```', content, re.DOTALL)
525
+ if match:
526
+ try:
527
+ task_info = json.loads(match.group(1))
528
+ task_type = task_info.get('type', 'Unknown')
529
+ task_label = task_info.get('label', 'Unknown')
530
+ task_status = task_info.get('status', 'Unknown')
531
+ accumulated_tool.append(f"\n🤖 **Agent Task** [{task_status}]: {task_type} - {task_label}\n")
532
+ except:
533
+ accumulated_tool.append(f"\n🤖 **Agent Task**: {content}\n")
534
+ elif "ping - " in content:
535
+ # Extract timestamp from ping messages
536
+ timestamp = content.split("ping - ")[-1]
537
+ accumulated_tool.append(f"⏱️ **Progress Update**: {timestamp}\n")
538
+ elif data_obj.get("type") == "done":
539
+ # Task completion
540
+ accumulated_tool.append(f"\n🎯 **Task Done**: {content}\n")
541
+ else:
542
+ # Regular result content - accumulate text for better readability
543
+ if not hasattr(callback_func, '_result_buffer'):
544
+ callback_func._result_buffer = ""
545
+ callback_func._result_buffer += content
546
+
547
+ # For simple text streams (like health check or mock mock), update more frequently
548
+ # Check if this is a simple text response (not BDD format)
549
+ is_simple_text = not any(marker in content for marker in ['```bdd-', 'ping -', 'data:'])
550
+
551
+ # For simple text streams, always update immediately to show all fragments
552
+ if is_simple_text and content.strip():
553
+ # Clear previous streaming text entries and add updated one
554
+ accumulated_tool = [item for item in accumulated_tool if not item.startswith("\n📊 **Streaming Text**:")]
555
+
556
+ # Add the updated complete streaming text in one line
557
+ accumulated_tool.append(f"\n📊 **Streaming Text**: {callback_func._result_buffer}\n")
558
+
559
+ # Immediate UI update for text streams
560
+ with tool_placeholder.expander("🔧 Tool Call Information", expanded=True):
561
+ st.markdown("".join(accumulated_tool))
562
+ else:
563
+ # For complex content, use timed updates
564
+ update_interval = 0.2 if len(content.strip()) <= 10 else 0.5
565
+
566
+ # Only update display periodically to avoid excessive updates
567
+ if not hasattr(callback_func, '_last_update_time'):
568
+ callback_func._last_update_time = 0
569
+
570
+ import time
571
+ current_time = time.time()
572
+ if current_time - callback_func._last_update_time > update_interval:
573
+ # For complex content, show accumulated buffer
574
+ accumulated_tool.append(f"\n📊 **Result Update**:\n")
575
+ accumulated_tool.append(f"```\n{callback_func._result_buffer}\n```\n")
576
+ callback_func._last_update_time = current_time
577
+
578
+ # Real-time UI update
579
+ with tool_placeholder.expander("🔧 Tool Call Information", expanded=True):
580
+ st.markdown("".join(accumulated_tool))
581
+ else:
582
+ # Handle other data types that are not "result" type
583
+ # This ensures ALL data: messages are processed and displayed
584
+ data_type = data_obj.get("type", "unknown")
585
+ data_content = data_obj.get("content", str(data_obj))
586
+
587
+ # Add timestamp for real-time tracking
588
+ import time
589
+ timestamp = time.strftime("%H:%M:%S")
590
+
591
+ # Format the data for display
592
+ data_entry = ""
593
+ if isinstance(data_content, str):
594
+ data_entry = f"\n📡 **Data [{data_type}]** [{timestamp}]: {data_content}\n"
595
+ else:
596
+ data_entry = f"\n📡 **Data [{data_type}]** [{timestamp}]:\n```json\n{json.dumps(data_obj, indent=2)}\n```\n"
597
+
598
+ # Add to both temporary and persistent storage
599
+ accumulated_tool.append(data_entry)
600
+ callback_func._persistent_data.append(data_entry)
601
+
602
+ # Immediate real-time UI update for any data: message
603
+ with tool_placeholder.expander("🔧 Tool Call Information", expanded=True):
604
+ # Show data counter at the top
605
+ st.markdown(f"**📊 Total Data Messages: {callback_func._data_counter}**")
606
+ st.markdown("---")
607
+ # Show persistent data first, then current accumulated data
608
+ st.markdown("".join(callback_func._persistent_data))
609
+ st.markdown("---")
610
+ st.markdown("**🔄 Current Stream:**")
611
+ st.markdown("".join(accumulated_tool))
612
+ else:
613
+ # Handle non-dict data objects
614
+ import time
615
+ timestamp = time.strftime("%H:%M:%S")
616
+ data_entry = f"\n📡 **Raw Data** [{timestamp}]:\n```json\n{json_str}\n```\n"
617
+
618
+ # Add to both temporary and persistent storage
619
+ accumulated_tool.append(data_entry)
620
+ callback_func._persistent_data.append(data_entry)
621
+
622
+ # Immediate real-time UI update
623
+ with tool_placeholder.expander("🔧 Tool Call Information", expanded=True):
624
+ # Show data counter at the top
625
+ st.markdown(f"**📊 Total Data Messages: {callback_func._data_counter}**")
626
+ st.markdown("---")
627
+ # Show persistent data first, then current accumulated data
628
+ st.markdown("".join(callback_func._persistent_data))
629
+ st.markdown("---")
630
+ st.markdown("**🔄 Current Stream:**")
631
+ st.markdown("".join(accumulated_tool))
632
+ except json.JSONDecodeError:
633
+ # If not valid JSON, check if it's streaming text content
634
+ if json_str and len(json_str.strip()) > 0:
635
+ # This might be streaming text, accumulate it
636
+ if not hasattr(callback_func, '_stream_buffer'):
637
+ callback_func._stream_buffer = ""
638
+ callback_func._stream_buffer += json_str
639
+
640
+ # Only show streaming content periodically
641
+ if not hasattr(callback_func, '_stream_update_time'):
642
+ callback_func._stream_update_time = 0
643
+
644
+ import time
645
+ current_time = time.time()
646
+ if current_time - callback_func._stream_update_time > 0.3: # Update every 0.3 seconds for better responsiveness
647
+ # Add new streaming update without clearing previous ones
648
+ if callback_func._stream_buffer.strip():
649
+ accumulated_tool.append(f"\n📝 **Streaming Update**: {callback_func._stream_buffer}\n")
650
+ callback_func._stream_update_time = current_time
651
+
652
+ # Real-time UI update
653
+ with tool_placeholder.expander("🔧 Tool Call Information", expanded=True):
654
+ st.markdown("".join(accumulated_tool))
655
+ else:
656
+ # Handle empty or whitespace-only data
657
+ import time
658
+ timestamp = time.strftime("%H:%M:%S")
659
+ accumulated_tool.append(f"\n📡 **Empty Data** [{timestamp}]: (empty or whitespace)\n")
660
+
661
+ # Immediate real-time UI update
662
+ with tool_placeholder.expander("🔧 Tool Call Information", expanded=True):
663
+ st.markdown("".join(accumulated_tool))
664
+ except Exception as e:
665
+ # Fallback: treat as plain text, but only if it's meaningful
666
+ import time
667
+ timestamp = time.strftime("%H:%M:%S")
668
+ if line.strip() and len(line.strip()) > 1: # Only show non-trivial content
669
+ accumulated_tool.append(f"\n📝 **Info** [{timestamp}]: {line.strip()}\n")
670
+ else:
671
+ accumulated_tool.append(f"\n⚠️ **Error** [{timestamp}]: {str(e)}\n")
672
+
673
+ # Immediate real-time UI update for error cases
674
+ with tool_placeholder.expander("🔧 Tool Call Information", expanded=True):
675
+ st.markdown("".join(accumulated_tool))
676
+ elif line.startswith('ping - '):
677
+ # Handle ping messages directly
678
+ timestamp = line.split('ping - ')[-1]
679
+ accumulated_tool.append(f"⏱️ **Progress Update**: {timestamp}\n")
680
+
681
+ # Immediate real-time UI update for ping messages
682
+ with tool_placeholder.expander("🔧 Tool Call Information", expanded=True):
683
+ st.markdown("".join(accumulated_tool))
684
+ elif line and not line.startswith(':'):
685
+ # Other non-empty lines - capture any other data patterns
686
+ import time
687
+ timestamp = time.strftime("%H:%M:%S")
688
+
689
+ # Check if this line contains any meaningful data
690
+ if line.strip() and len(line.strip()) > 1:
691
+ # Try to detect if it's JSON-like content
692
+ if line.strip().startswith('{') or line.strip().startswith('['):
693
+ try:
694
+ # Try to parse as JSON for better formatting
695
+ import json
696
+ parsed_json = json.loads(line.strip())
697
+ accumulated_tool.append(f"\n📡 **JSON Data** [{timestamp}]:\n```json\n{json.dumps(parsed_json, indent=2)}\n```\n")
698
+ except:
699
+ # If not valid JSON, show as regular data
700
+ accumulated_tool.append(f"\n📡 **Data** [{timestamp}]: {line.strip()}\n")
701
+ else:
702
+ # Regular text data
703
+ accumulated_tool.append(f"\n📝 **Info** [{timestamp}]: {line.strip()}\n")
704
+
705
+ # Immediate real-time UI update for any captured data
706
+ with tool_placeholder.expander("🔧 Tool Call Information", expanded=True):
707
+ st.markdown("".join(accumulated_tool))
708
+ else:
709
+ # Regular tool response content
710
+ accumulated_tool.append(
711
+ "\n```json\n" + str(tool_content) + "\n```\n"
712
+ )
713
+ else:
714
+ # Non-string content
715
+ accumulated_tool.append(
716
+ "\n```json\n" + str(tool_content) + "\n```\n"
717
+ )
718
+
719
+ # Show pending tool completion status after all streaming content
720
+ if hasattr(callback_func, '_pending_tool_completion') and callback_func._pending_tool_completion:
721
+ for tool_name in callback_func._pending_tool_completion:
722
+ accumulated_tool.append(f"\n✅ **Tool Completed**: {tool_name}\n")
723
+ # Clear the pending list
724
+ callback_func._pending_tool_completion = []
725
+
726
+
727
+
728
  return None
729
 
730
  return callback_func, accumulated_text, accumulated_tool
 
767
  timeout=timeout_seconds,
768
  )
769
  except asyncio.TimeoutError:
770
+ # On timeout, reset thread to avoid leaving an incomplete tool call in memory
771
+ st.session_state.thread_id = random_uuid()
772
+ error_msg = (
773
+ f"⏱️ Request time exceeded {timeout_seconds} seconds. Conversation was reset. Please retry."
774
+ )
775
  return {"error": error_msg}, error_msg, ""
776
+ except ValueError as e:
777
+ # Handle invalid chat history caused by incomplete tool calls
778
+ if "Found AIMessages with tool_calls" in str(e):
779
+ # Reset thread and retry once
780
+ st.session_state.thread_id = random_uuid()
781
+ try:
782
+ response = await asyncio.wait_for(
783
+ astream_graph(
784
+ st.session_state.agent,
785
+ {"messages": [HumanMessage(content=query)]},
786
+ callback=streaming_callback,
787
+ config=RunnableConfig(
788
+ recursion_limit=st.session_state.recursion_limit,
789
+ thread_id=st.session_state.thread_id,
790
+ ),
791
+ ),
792
+ timeout=timeout_seconds,
793
+ )
794
+ except Exception:
795
+ error_msg = (
796
+ "⚠️ Conversation state was invalid and has been reset. Please try again."
797
+ )
798
+ return {"error": error_msg}, error_msg, ""
799
+ else:
800
+ raise
801
 
802
  final_text = "".join(accumulated_text_obj)
803
  final_tool = "".join(accumulated_tool_obj)
 
832
  if mcp_config is None:
833
  # Load settings from config.json file
834
  mcp_config = load_config_from_json()
835
+
836
  client = MultiServerMCPClient(mcp_config)
837
  await client.__aenter__()
838
  tools = client.get_tools()
 
843
  selected_model = st.session_state.selected_model
844
 
845
  if selected_model in [
846
+ "claude-3-5-sonnet-20241022",
847
  "claude-3-5-sonnet-latest",
848
  "claude-3-5-haiku-latest",
849
  ]:
 
854
  )
855
  else: # Use OpenAI model
856
  model = ChatOpenAI(
857
+ base_url=os.environ.get("OPENAI_API_BASE"),
858
  model=selected_model,
859
  temperature=0.1,
860
  max_tokens=OUTPUT_TOKEN_INFO[selected_model]["max_tokens"],
 
883
  if has_anthropic_key:
884
  available_models.extend(
885
  [
886
+ "claude-3-5-sonnet-20241022",
887
  "claude-3-5-sonnet-latest",
888
  "claude-3-5-haiku-latest",
889
  ]
 
900
  "⚠️ API keys are not configured. Please add ANTHROPIC_API_KEY or OPENAI_API_KEY to your .env file."
901
  )
902
  # Add Claude model as default (to show UI even without keys)
903
+ available_models = ["claude-3-5-sonnet-20241022"]
904
 
905
  # Model selection dropdown
906
  previous_model = st.session_state.selected_model
 
928
  st.session_state.timeout_seconds = st.slider(
929
  "⏱️ Response generation time limit (seconds)",
930
  min_value=60,
931
+ max_value=300000,
932
  value=st.session_state.timeout_seconds,
933
  step=10,
934
  help="Set the maximum time for the agent to generate a response. Complex tasks may require more time.",
 
1041
  st.info(
1042
  f"URL detected in '{tool_name}' tool, setting transport to 'sse'."
1043
  )
1044
+
1045
  elif "transport" not in tool_config:
1046
  # Set default "stdio" if URL doesn't exist and transport isn't specified
1047
  tool_config["transport"] = "stdio"
app_KOR.py DELETED
@@ -1,848 +0,0 @@
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
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
config.json CHANGED
@@ -5,5 +5,13 @@
5
  "./mcp_server_time.py"
6
  ],
7
  "transport": "stdio"
 
 
 
 
 
 
 
 
8
  }
9
  }
 
5
  "./mcp_server_time.py"
6
  ],
7
  "transport": "stdio"
8
+ },
9
+ "qa": {
10
+ "transport": "sse",
11
+ "url": "http://10.15.56.148:8000/qa"
12
+ },
13
+ "review_generate": {
14
+ "transport": "sse",
15
+ "url": "http://10.15.56.148:8000/review"
16
  }
17
  }
run.sh ADDED
@@ -0,0 +1 @@
 
 
1
+ streamlit run app.py --server.address=0.0.0.0