mcp_test/app/tools/agent_tools.py
sun feb1a0b280 feat(agent): 新增MCP Agent客户端和工具系统
- 实现了基于LangChain的MCP Agent,支持连接MCP服务器调用工具
- 添加了环境配置文件(.env),包含LLM模型和API配置信息
- 创建了完整的工具系统,包括BaseTool基类和Bash、Terminate、Add等工具
- 集成了天气查询工具,支持通过中国气象局API获取天气预报信息
- 实现了交互式对话功能,支持多轮工具调用和结果处理
- 添加了详细的CLAUDE.md开发指导文档
2026-02-25 18:04:48 +08:00

190 lines
9.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from langchain.agents.middleware import wrap_tool_call
from langchain.tools import tool
from typing import Dict, Optional, Union
import requests
import os
from dotenv import load_dotenv, find_dotenv
from langchain_core.messages import ToolMessage
_ = load_dotenv(find_dotenv())
@tool
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
@tool
def get_weather_by_location(province: str, place: str) -> Dict[
str, Union[str, float, int]
]:
"""
调用中国气象局天气预报API获取指定省份和地点的完整当日天气信息含基础预报、实时数据、气象预警
### 参数说明
- province: str - 查询省份/直辖市,建议去除""""后缀(例:"四川"而非"四川省""北京"而非"北京市"
- place: str - 查询城市/区/县,建议去除""""""后缀(例:"绵阳"而非"绵阳市""大兴"而非"大兴区"
### 返回说明成功code=200
基础预报信息:
- code: int - 状态码成功固定为200
- guo: str - 国家名称(例:"中国"
- sheng: str - 标准化省份/直辖市名称(例:"四川"
- shi: str - 标准化城市/地区名称(例:"绵阳"
- name: str - 与shi一致冗余字段"绵阳"
- weather1: str - 当日主要天气(例:"阵雨"
- weather2: str - 当日次要天气(例:"阵雨"
- wd1: str - 当日最高温度(单位:℃,例:"25"
- wd2: str - 当日最低温度(单位:℃,例:"18"
- winddirection1: str - 当日主要风向(例:"无持续风向"
- winddirection2: str - 当日次要风向(例:"无持续风向"
- windleve1: str - 当日主要风力等级(例:"微风"
- windleve2: str - 当日次要风力等级(例:"微风"
- weather1img: str - 主要天气图标URL"https://rescdn.apihz.cn/resimg/tianqi/zhenyu.png"
- weather2img: str - 次要天气图标URL"https://rescdn.apihz.cn/resimg/tianqi/zhenyu.png"
- lon: str - 地区经度保留3位小数"104.730"
- lat: str - 地区纬度保留3位小数"31.440"
- uptime: str - 预报数据更新时间格式YYYY-MM-DD HH:MM:SS"2025-08-29 12:00:00"
实时天气数据(顶层字段):
- now_precipitation: float - 当前降水量单位mm0.0
- now_temperature: float - 当前温度单位19.3
- now_pressure: int - 当前气压单位hPa956
- now_humidity: int - 当前湿度(单位:%85
- now_windDirection: str - 当前风向(例:"东北风"
- now_windDirectionDegree: int - 当前风向角度0°=北风28
- now_windSpeed: float - 当前风速单位m/s3.2
- now_windScale: str - 当前风力等级(例:"微风"
- now_feelst: float - 当前体感温度单位19.7
- now_uptime: str - 实时数据更新时间格式YYYY/MM/DD HH:MM"2025/08/29 10:05"
气象预警(顶层字段,无预警时为空字符串):
- alarm_id: str - 预警唯一ID"51070041600000_20250828215515"
- alarm_title: str - 预警标题(例:"绵阳市气象台更新大风蓝色预警信号[IV级/一般]"
- alarm_signaltype: str - 预警类型(例:"大风"
- alarm_signallevel: str - 预警等级(例:"蓝色"
- alarm_effective: str - 预警生效时间(例:"2025/08/28 21:55"
- alarm_eventType: str - 预警事件编码(例:"11B06"
- alarm_severity: str - 预警等级英文编码(例:"BLUE"
- alarm_type: str - 预警类型编码(例:"p0007004"
### 返回说明失败code=400
- code: int - 错误状态码固定为400
- msg: str - 错误详情(例:"通讯秘钥错误。""API响应解析失败"
"""
api_url = "https://cn.apihz.cn/api/tianqi/tqyb.php"
clean_province = province.replace("", "").replace("", "").strip()
clean_place = place.replace("", "").replace("", "").replace("", "").strip()
final_user_id = os.getenv("APIHZ_ID")
final_user_key = os.getenv("APIHZ_KEY")
params = {
"id": final_user_id,
"key": final_user_key,
"sheng": clean_province,
"place": clean_place
}
try:
# 1. 发送请求(增加超时重试,避免偶发网络波动)
response = requests.get(api_url, params=params, timeout=10)
response.raise_for_status() # 捕获4xx/5xx HTTP错误
# 2. 解析响应先判断响应内容是否为空再转JSON
response_text = response.text.strip()
if not response_text:
return {"code": 400, "msg": "API响应为空无法解析天气数据"}
# 3. 转JSON并防御None确保weather_data是字典
weather_data = response.json()
if not isinstance(weather_data, dict):
return {"code": 400, "msg": f"API响应格式错误不是有效字典实际类型{type(weather_data).__name__}"}
# 4. 处理API返回的错误状态code=400
if weather_data.get("code") == 400:
return {
"code": 400,
"msg": weather_data.get("msg", "API返回错误原因未知")
}
# 5. 确保API返回成功状态code=200
elif weather_data.get("code") != 200:
return {
"code": 400,
"msg": f"API返回非成功状态码{weather_data.get('code', '未知')}"
}
# 6. 提取嵌套数据确保now_info/alarm_info是字典避免None
now_info = weather_data.get("nowinfo", {})
if not isinstance(now_info, dict):
now_info = {} # 若now_info不是字典强制设为空字典
alarm_info = weather_data.get("alarm", {})
if not isinstance(alarm_info, dict):
alarm_info = {} # 若alarm不是字典强制设为空字典
# 7. 构造最终返回结果(全部顶层字段,无嵌套)
return {
# 基础预报字段
"code": 200,
"guo": weather_data.get("guo", ""),
"sheng": weather_data.get("sheng", ""),
"shi": weather_data.get("shi", ""),
"name": weather_data.get("name", ""),
"weather1": weather_data.get("weather1", ""),
"weather2": weather_data.get("weather2", ""),
"wd1": weather_data.get("wd1", ""),
"wd2": weather_data.get("wd2", ""),
"winddirection1": weather_data.get("winddirection1", ""),
"winddirection2": weather_data.get("winddirection2", ""),
"windleve1": weather_data.get("windleve1", ""),
"windleve2": weather_data.get("windleve2", ""),
"weather1img": weather_data.get("weather1img", ""),
"weather2img": weather_data.get("weather2img", ""),
"lon": weather_data.get("lon", ""),
"lat": weather_data.get("lat", ""),
"uptime": weather_data.get("uptime", ""),
# 实时天气字段
"now_precipitation": float(now_info.get("precipitation", 0.0)),
"now_temperature": float(now_info.get("temperature", 0.0)),
"now_pressure": int(now_info.get("pressure", 0)),
"now_humidity": int(now_info.get("humidity", 0)),
"now_windDirection": now_info.get("windDirection", ""),
"now_windDirectionDegree": int(now_info.get("windDirectionDegree", 0)),
"now_windSpeed": float(now_info.get("windSpeed", 0.0)),
"now_windScale": now_info.get("windScale", ""),
"now_feelst": float(now_info.get("feelst", 0.0)),
"now_uptime": now_info.get("uptime", ""),
# 预警字段
"alarm_id": alarm_info.get("id", ""),
"alarm_title": alarm_info.get("title", ""),
"alarm_signaltype": alarm_info.get("signaltype", ""),
"alarm_signallevel": alarm_info.get("signallevel", ""),
"alarm_effective": alarm_info.get("effective", ""),
"alarm_eventType": alarm_info.get("eventType", ""),
"alarm_severity": alarm_info.get("severity", ""),
"alarm_type": alarm_info.get("type", "")
}
# 8. 捕获各类异常(明确错误原因)
except requests.exceptions.HTTPError as e:
status_code = response.status_code if "response" in locals() else "未知"
return {"code": 400, "msg": f"HTTP请求错误状态码{status_code}{str(e)}"}
except requests.exceptions.ConnectionError:
return {"code": 400, "msg": "网络连接错误无法连接到天气API服务器"}
except requests.exceptions.Timeout:
return {"code": 400, "msg": "请求超时API服务器10秒内未响应"}
except ValueError as e:
# 捕获JSON解析错误如响应不是合法JSON
return {"code": 400, "msg": f"API响应解析失败JSON格式错误{str(e)}"}
except Exception as e:
# 捕获其他未知错误(附带具体错误信息,便于调试)
return {"code": 400, "msg": f"未知错误:{str(e)}(错误类型:{type(e).__name__}"}
@wrap_tool_call
def handle_tool_errors(request, handler):
"""使用自定义消息处理工具执行错误。"""
try:
return handler(request)
except Exception as e:
# 向模型返回自定义错误消息
return ToolMessage(
content=f"工具错误:请检查您的输入并重试。({str(e)})",
tool_call_id=request.tool_call["id"]
)