12-day3-ReAct Agent-实验3-交互式-6个tools

✅ 升级版 react_agent_2.py

新增功能

  1. 本地冷笑话库:程序启动时自动创建 jokes.txt(若不存在),内含 10 个程序员冷笑话
  2. tell_a_joke 工具:从本地文件随机读取一条笑话
  3. get_my_ip_info 工具:查询公网 IP 和地理位置(使用 ipinfo.io
  4. run_safe_shell_command 工具:安全执行白名单内的 shell 命令

安全 & 鲁棒性

  • 外挂幽默笑话
  • Shell 命令严格白名单 + 超时 + 输出截断
  • 所有工具带异常处理
tee jokes.txt <<'EOF'
我问奶奶:“您年轻时怎么追爷爷的?”  
她眯着眼说:“哪用追?他欠我五块钱,三十年没还,只好娶我了。”

朋友失恋后去庙里求签,大师说:“放下。”  
他点点头,把前女友送的金镯子摘下来放功德箱里,转身就走。  
大师急了:“我不是让你放下这个!”

我妈坚信WiFi辐射会让人变傻。  
昨天她拔了路由器,结果我爸立刻清醒过来,第一件事就是问:“老婆,你藏私房钱那罐茶叶在哪?”

相亲对象问我:“你有什么缺点?”  
我说:“太诚实。”  
她说:“这不算缺点。”  
我说:“你丑。”

小区电梯里,一位大妈盯着我看半天,突然说:  
“小伙子,你长得好像我儿子!”  
我感动地说:“真的吗?”  
她叹口气:“可惜他三年前就去世了……”  
我赶紧安慰她。  
她接着说:“不过没关系,他留下的房贷,现在由你继承。”

老板说:“公司就是你的家!”  
第二天我就把办公椅搬回家了。  
他说:“你怎么能这样?”  
我说:“你说这是我家啊。”

女朋友生日,我送她一本《如何读懂男人的心》。  
她翻了两页,抬头问我:“你确定这是新书?怎么第38页折了角?”  
我说:“……那是我昨晚试读时折的。”

我爸退休后沉迷短视频,最近总对我说:  
“儿子,你要努力!你看人家马化腾,40岁就财务自由了!”  
我说:“爸,马化腾今年53。”  
他愣了一下:“那更要抓紧了!只剩13年了!”

医生对病人说:“你得戒烟戒酒,早睡早起,保持心情愉快。”  
病人问:“然后呢?”  
医生说:“然后你就能活得久一点——多受几年罪。”

小时候偷吃糖被妈妈发现,她问:“是不是你?”  
我摇头。  
她说:“那糖怎么少了?”  
我说:“可能是糖自己想出去看看世界。”  
现在我在做产品经理,专门给APP加“自动续费”功能。
EOF
tee react_agent_3.py <<'EOF'
# react_agent_3.py - 增强交互式 ReAct Agent
import os
import platform
import random
import subprocess
from datetime import datetime
from dotenv import load_dotenv
from langchain_core.tools import tool
from langchain.agents import AgentExecutor, create_react_agent
from langchain import hub
from langchain_openai import ChatOpenAI
import requests

# 加载 API Key
load_dotenv()


# ────────────────────────────────────────────────
# 🔧【工具 1:查询包在清华源是否可用】
# ────────────────────────────────────────────────
@tool
def check_package_on_tsinghua_mirror(package_name: str) -> str:
    """
    检查指定的 Python 包是否在清华大学 PyPI 镜像中可用。
    示例输出: "numpy: ✅ 可用,最新版本 1.26.4"
    """
    try:
        url = f"https://pypi.tuna.tsinghua.edu.cn/{package_name}/json"
        headers = {"User-Agent": "langchain-agent"}
        response = requests.get(url, headers=headers, timeout=8)

        if response.status_code == 200:
            data = response.json()
            version = data["info"]["version"]
            return f"{package_name}: ✅ 可用,最新版本 {version}"
        elif response.status_code == 404:
            return f"{package_name}: ❌ 未在 PyPI 或清华源中找到"
        else:
            return f"{package_name}: ⚠️ 镜像服务返回错误 {response.status_code}"
    except Exception as e:
        return f"查询失败: {str(e)}"

# ────────────────────────────────────────────────
# 🕒【工具 2:获取当前时间】
# ────────────────────────────────────────────────
@tool
def get_current_time(dummy: str = "") -> str:
    """获取当前日期和时间(中国标准时间)"""
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    return f"{now} (CST)"

# 💻【工具 3:获取详细操作系统信息】
@tool
def get_os_info(dummy: str = "") -> str:
    """返回当前操作系统的详细类型和版本"""
    system = platform.system()
    
    if system == "Windows":
        return "Windows"
    elif system == "Darwin":
        return "macOS"
    elif system == "Linux":
        release_files = ["/etc/os-release", "/etc/lsb-release", "/etc/redhat-release", "/etc/debian_version"]
        for filepath in release_files:
            try:
                with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
                    content = f.read().strip()
                    if not content:
                        continue
                    if filepath == "/etc/os-release":
                        props = {}
                        for line in content.splitlines():
                            if "=" in line and not line.startswith("#"):
                                k, v = line.split("=", 1)
                                props[k] = v.strip('"').strip("'")
                        name = props.get("PRETTY_NAME") or props.get("NAME") or "Linux"
                        version = props.get("VERSION", "")
                        full = f"{name} {version}".strip()
                        return full if full else "Linux"
                    else:
                        first_line = content.split("\n")[0].strip()
                        if first_line:
                            return first_line
            except (FileNotFoundError, PermissionError, OSError):
                continue
        return "Linux"
    else:
        return f"其他系统: {system}"

# 🌍【工具 4:获取公网 IP 和地理位置】
@tool
def get_my_ip_info(dummy: str = "") -> str:
    """获取当前公网 IP 及大致地理位置(基于 ipinfo.io)"""
    try:
        response = requests.get("https://ipinfo.io/json", timeout=5)
        if response.status_code == 200:
            data = response.json()
            ip = data.get("ip", "未知")
            city = data.get("city", "")
            region = data.get("region", "")
            country = data.get("country", "")
            loc = ", ".join(filter(None, [city, region, country])) or "未知位置"
            return f"公网 IP: {ip},位置: {loc}"
        else:
            return "IP 查询服务暂不可用"
    except Exception as e:
        return f"IP 查询失败: {str(e)}"

# 💀【工具:讲一个本地幽默故事】
@tool
def tell_a_joke(dummy: str = "") -> str:
    """
    从本地 jokes.txt 随机讲一个幽默故事。
    文件格式:每个故事占一个或多个连续非空行,故事之间用空行分隔。
    """
    try:
        with open("jokes.txt", "r", encoding="utf-8") as f:
            content = f.read()

        # 按空行(\n\n 或更多)分割段落,并清理每段
        paragraphs = [p.strip() for p in content.split("\n\n") if p.strip()]
        
        if not paragraphs:
            return "故事库为空,请检查 jokes.txt 是否有内容。"

        story = random.choice(paragraphs)
        return "🎭 幽默时间:\n" + story

    except FileNotFoundError:
        return "未找到 jokes.txt,请先创建它!"
    except Exception as e:
        return f"加载故事失败: {str(e)}"

# 🖥️【工具 6:安全执行 Shell 命令(白名单)】
@tool
def run_safe_shell_command(cmd: str) -> str:
    """
    执行安全的 shell 命令(仅限白名单)
    支持: 'ls', 'pwd', 'whoami', 'date', 'df', 'free', 'uname'
    """
    allowed = {
        "ls": ["ls", "-l"],
        "pwd": ["pwd"],
        "whoami": ["whoami"],
        "date": ["date"],
        "df": ["df", "-h"],
        "free": ["free", "-h"],
        "uname": ["uname", "-a"]
    }
    
    cmd_key = cmd.strip().split()[0] if cmd.strip() else ""
    if cmd_key not in allowed:
        return f"不支持的命令: '{cmd}'。可用命令: {', '.join(allowed.keys())}"
    
    try:
        result = subprocess.run(
            allowed[cmd_key],
            capture_output=True,
            text=True,
            timeout=5,
            cwd=os.getcwd()
        )
        output = (result.stdout or result.stderr).strip()
        # 截断长输出
        if len(output) > 800:
            output = output[:800] + "...\n(输出已截断)"
        return output if output else "命令执行成功,无输出。"
    except subprocess.TimeoutExpired:
        return "命令执行超时"
    except Exception as e:
        return f"命令执行失败: {str(e)}"

# ────────────────────────────────────────────────
# 🧠【LLM 配置:使用 Qwen via DashScope】
# ────────────────────────────────────────────────
llm = ChatOpenAI(
    model="qwen-max",
    temperature=0,
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    api_key=os.getenv("DASHSCOPE_API_KEY")
)

# 注册所有工具(按逻辑分组)
tools = [
    check_package_on_tsinghua_mirror,
    get_current_time,
    get_os_info,
    get_my_ip_info,
    tell_a_joke,
    run_safe_shell_command
]

# ────────────────────────────────────────────────
# 📜【加载 ReAct 提示模板】
# ────────────────────────────────────────────────
prompt = hub.pull("hwchase17/react")

# ────────────────────────────────────────────────
# 🤖【创建 ReAct Agent】
# ────────────────────────────────────────────────
agent = create_react_agent(llm, tools, prompt)

# ────────────────────────────────────────────────
# ⚙️【创建执行器】
# ────────────────────────────────────────────────
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    handle_parsing_errors=True,
    max_iterations=8  # 支持更复杂的多工具组合
)

# ────────────────────────────────────────────────
# 💬【交互式主循环】
# ────────────────────────────────────────────────
print("🚀 ReAct Agent 已启动!支持以下能力:")
print("  • 查询 PyPI 包(清华源)")
print("  • 获取当前时间")
print("  • 识别操作系统详情")
print("  • 查询公网 IP 与地理位置")
print("  • 讲一个程序员冷笑话")
print("  • 安全执行 shell 命令(ls/pwd/whoami/df/free/uname)")
print("\n💡 尝试提问:")
print("   - '我在哪?'")
print("   - '讲个笑话!'")
print("   - '当前目录有什么文件?'")
print("\n输入 'quit' 或 'exit' 退出\n")

while True:
    try:
        user_input = input("❓ 你: ").strip()
        if not user_input:
            continue
        if user_input.lower() in ("quit", "exit", "q"):
            print("👋 再见!")
            break

        print("\n🧠 Agent 正在思考...\n")
        result = agent_executor.invoke({"input": user_input})

        print("\n" + "=" * 60)
        print("🤖 Agent 回答:")
        print(result["output"])
        print("\n" + "—" * 60 + "\n")

    except KeyboardInterrupt:
        print("\n\n👋 用户中断,再见!")
        break
    except Exception as e:
        print(f"\n❌ 运行出错: {e}\n")
EOF

✅ 新增能力一览

工具 调用方式 示例问题
tell_a_joke 本地文件 “讲个笑话!”、“我需要快乐”
get_my_ip_info 网络 API “我在哪?”、“我的公网 IP 是多少?”
run_safe_shell_command 白名单命令 “当前目录有什么?”、“磁盘空间还剩多少?”

🔒 安全说明

  • Shell 命令:仅允许 7 个无害命令,禁止任意执行
  • 文件读取:仅读取当前目录下的 jokes.txt,无写入或遍历
  • 网络请求:所有外部调用均有超时和异常捕获

posted @ 2026-01-27 23:16  船山薪火  阅读(17)  评论(0)    收藏  举报
![image](https://img2024.cnblogs.com/blog/3174785/202601/3174785-20260125205854513-941832118.jpg)