129 lines
4.4 KiB
Python
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()) |