feat: add module 10 code and related content
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
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())
|
||||
Reference in New Issue
Block a user