Files
agent-craft/m10_mcp_basics/mcp_main.py
T

129 lines
4.4 KiB
Python

import os
import sys
from contextlib import AsyncExitStack
import asyncio
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage
from langgraph.graph import StateGraph,MessagesState,START,END
from langgraph.prebuilt import ToolNode
from config import OPENAI_API_KEY,AMAP_MAPS_API_KEY
from m10_mcp_basics.agent_stream import run_agent_with_streaming
from m10_mcp_basics.mcp_client import MCPClient
from m10_mcp_basics.mcp_bridge import LangChainMCPAdapter
# ===环境配置===
# 环境兼容
COMMAND = "npx.cmd" if sys.platform == "win32" else "npx"
# 复制当前py进程的环境变量,并在复制的环境变量里新增一条,确保安全可控
env_vars = os.environ.copy()
env_vars["AMAP_MAPS_API_KEY"] = AMAP_MAPS_API_KEY
MCP_SERVER_CONFIGS = [
{
"name":"高德地图", # 打印使用了什么MCP,可移除
"command":COMMAND,
"args":["-y", "@amap/amap-maps-mcp-server"],
"env":env_vars
}
# {...} 之后MCP工具可随需求扩展增加
]
# ===构建图逻辑===
def build_graph(available_tools):
"""
这个函数只认tools列表,不关心tools的来源
"""
if not available_tools:
print('⚠️ 当前没有注入任何工具,Agent将仅靠LLM回答。')
llm = ChatOpenAI(
model="deepseek-chat",
api_key=OPENAI_API_KEY,
base_url="https://api.deepseek.com",
streaming=True
)
# 如果没工具,bind_tools 会被忽略或处理,LangGraph同样能正常跑纯对话
llm_with_tools = llm.bind_tools(available_tools) if available_tools else llm
sys_prompt = """
你是一个专业的地理位置服务助手。
1. 当用户查询模糊地点(如"西站")时,会优先使用相关工具获取具体经纬度或标准名称。
2. 如果用户查询"附近"的店铺,请先确定中心点的坐标或具体位置,再进行搜索。
3. 调用工具时,参数要尽可能精确。
"""
async def agent_node(state:MessagesState):
messages = [SystemMessage(content=sys_prompt)] + state["messages"]
# ainvoke:异步调用版的invoke
return {"messages":[await llm_with_tools.ainvoke(messages)]}
workflow = StateGraph(MessagesState)
workflow.add_node("agent",agent_node)
# 动态逻辑:如果有工具才加工具节点,否则就是纯对话
if available_tools:
tool_node = ToolNode(available_tools)
workflow.add_node("tools",tool_node)
def should_continue(state:MessagesState):
last_msg = state["messages"][-1]
if hasattr(last_msg,"tool_calls") and last_msg.tool_calls:
return "tools"
return END
workflow.add_edge(START,"agent")
workflow.add_conditional_edges("agent",should_continue,{"tools":"tools",END:END})
workflow.add_edge("tools","agent")
else:
workflow.add_edge(START,"agent")
workflow.add_edge("agent",END)
return workflow.compile()
# ===MCP工具批量初始化===
async def load_mcp_tools(stack:AsyncExitStack,configs:list):
"""
负责遍历配置,批量建立连接,收集所有工具。
使用stack将连接生命周期托管给上层
"""
all_tools = []
for conf in configs:
print(f'🔌 正在连接:{conf["name"]}...')
# 初始化 Client
client = MCPClient(
command=conf["command"],
args=conf["args"],
env=conf.get("env") # 可选参数
)
# 🔥:enter_async_context 替代了async with 缩进
# 这样无论有多少个MCP,代码层级都不会变深
adapter = await stack.enter_async_context(LangChainMCPAdapter(client))
# 批量获取一个MCP下的所有工具
tools = await adapter.get_tools()
print(f' ✅️ 获取工具{[t.name for t in tools]}')
all_tools.extend(tools)
return all_tools
# ===主程序===
async def main():
# 使用ExitStack统一管理所有资源的关闭
async with AsyncExitStack() as stack:
# A.插件(MCP)注入阶段 -- 允许为空
dynamic_tools = await load_mcp_tools(stack,MCP_SERVER_CONFIGS)
# B.图构建阶段
app = build_graph(available_tools=dynamic_tools)
# C.运行阶段(流式)
query = "帮我查一下杭州西湖附近的酒店"
await run_agent_with_streaming(app,query)
if __name__ == '__main__':
asyncio.run(main())