{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "ab1efa37",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "数据库连接成功\n",
      "   可用表：['employees', 'orders', 'products']\n",
      "\n",
      "SQL 工具包已加载（4 个工具）：\n",
      "   - sql_db_query\n",
      "   - sql_db_schema\n",
      "   - sql_db_list_tables\n",
      "   - sql_db_query_checker\n",
      "<class 'list'>\n",
      "\n",
      "NL2SQL Agent 创建完成，可以开始提问了！\n"
     ]
    }
   ],
   "source": [
    "import os\n",
    "from langchain_community.utilities import SQLDatabase\n",
    "from langchain_community.agent_toolkits import SQLDatabaseToolkit\n",
    "from langchain_openai import ChatOpenAI\n",
    "from langchain.agents import create_agent  # langchain 1.3.1 的新 API\n",
    "from dotenv import load_dotenv\n",
    "load_dotenv()\n",
    "# ============================================================\n",
    "# 第一步：连接数据库\n",
    "# ============================================================\n",
    "# SQLDatabase.from_uri 接受标准的数据库连接 URI\n",
    "# LangChain 内部会用 SQLAlchemy 管理连接池\n",
    "\n",
    "\n",
    "\n",
    "db_uri = (\n",
    "    f\"mysql+pymysql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}\"\n",
    "    f\"@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}\"\n",
    ")\n",
    "db = SQLDatabase.from_uri(db_uri)\n",
    "# print(f\"mysql+pymysql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}\")\n",
    "# print(f\"@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}\")\n",
    "\n",
    "# 验证连接：打印可用的表名\n",
    "print(f\"数据库连接成功\")\n",
    "print(f\"   可用表：{db.get_usable_table_names()}\")\n",
    "\n",
    "# ============================================================\n",
    "# 第二步：初始化大模型\n",
    "# ============================================================\n",
    "llm = ChatOpenAI(\n",
    "    model=\"deepseek-v4-flash\",\n",
    "    api_key=os.getenv(\"OPENAI_API_KEY\"),\n",
    "    base_url=\"https://api.deepseek.com\",\n",
    "    temperature=0,  # SQL 生成需要确定性输出\n",
    ")\n",
    "\n",
    "# ============================================================\n",
    "# 第三步：创建 SQL 工具包\n",
    "# ============================================================\n",
    "# SQLDatabaseToolkit 会自动注册 4 个工具：\n",
    "#   1. sql_db_list_tables    — 列出数据库中所有表\n",
    "#   2. sql_db_schema         — 获取指定表的 DDL 结构\n",
    "#   3. sql_db_query_checker  — 检查 SQL 语法是否正确\n",
    "#   4. sql_db_query          — 执行 SQL 并返回结果\n",
    "toolkit = SQLDatabaseToolkit(db=db, llm=llm)\n",
    "tools = toolkit.get_tools()\n",
    "\n",
    "print(f\"\\nSQL 工具包已加载（{len(tools)} 个工具）：\")\n",
    "for t in tools:\n",
    "    print(f\"   - {t.name}\")\n",
    "\n",
    "print(type (tools))\n",
    "# ============================================================\n",
    "# 第四步：定义 System Prompt（Agent 的\"岗位说明书\"）\n",
    "# ============================================================\n",
    "SQL_AGENT_PROMPT = \"\"\"你是一名专业的 SQL 数据分析师。\n",
    "\n",
    "## 工作流程\n",
    "1. 先用 sql_db_list_tables 查看数据库中有哪些表\n",
    "2. 用 sql_db_schema 获取相关表的字段结构和类型\n",
    "3. 生成 SQL 之前，用 sql_db_query_checker 检查语法\n",
    "4. 确认无误后，用 sql_db_query 执行查询\n",
    "5. 用中文总结查询结果，给出简洁的业务洞察\n",
    "\n",
    "## 约束\n",
    "- 只使用数据库中实际存在的表和字段，不要凭空编造\n",
    "- 单次查询结果限制在 50 条以内\n",
    "- 如果查询出错，分析错误原因后重新生成 SQL\n",
    "- 回答要简洁专业，不要啰嗦\n",
    "\"\"\"\n",
    "\n",
    "# ============================================================\n",
    "# 第五步：组装 Agent（langchain 1.3.1 一行搞定）\n",
    "# ============================================================\n",
    "# create_agent 返回一个可直接调用的 Runnable，无需再套 AgentExecutor\n",
    "# 它内部自动处理工具调用、循环迭代、错误重试等逻辑\n",
    "sql_agent = create_agent(\n",
    "    model=llm,\n",
    "    tools=tools,\n",
    "    system_prompt=SQL_AGENT_PROMPT,\n",
    ")\n",
    "\n",
    "print(\"\\nNL2SQL Agent 创建完成，可以开始提问了！\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "d7068f30",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "回答：公司目前共有 **5 名员工**。\n"
     ]
    }
   ],
   "source": [
    "response = sql_agent.invoke({\n",
    "    \"messages\": [{\"role\": \"user\", \"content\": \"公司一共有多少名员工？\"}]\n",
    "})\n",
    "\n",
    "# 最终回答在最后一条消息里\n",
    "final_msg = response[\"messages\"][-1]\n",
    "print(f\"回答：{final_msg.content}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "5989558d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "回答：查询结果如下：\n",
      "\n",
      "| 姓名 | 薪资 |\n",
      "|:---:|:---:|\n",
      "| 张三 | 20,000.00 |\n",
      "| 王五 | 16,000.00 |\n",
      "\n",
      "**业务洞察：**\n",
      "技术部共有 **2 名员工**，薪资总和为 36,000 元。其中：\n",
      "- **张三**（20,000 元）薪资最高，是技术部的核心骨干\n",
      "- **王五**（16,000 元）紧随其后，团队薪资差距不大（约 25%）\n"
     ]
    }
   ],
   "source": [
    "response = sql_agent.invoke({\n",
    "    \"messages\": [{\"role\": \"user\", \"content\": \"列出技术部所有员工的姓名和薪资，按薪资从高到低排序\"}]\n",
    "})\n",
    "final_msg = response[\"messages\"][-1]\n",
    "print(f\"回答：{final_msg.content}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d3b36789",
   "metadata": {},
   "outputs": [],
   "source": [
    "response = sql_agent.invoke({\n",
    "    \"messages\": [{\"role\": \"user\", \"content\": \"销售部的员工总共下了多少订单？订单总金额是多少？\"}]\n",
    "})\n",
    "final_msg = response[\"messages\"][-1]\n",
    "print(f\"回答：{final_msg.content}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "333a4697",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "最终回答：每个部门各有多少人？\n",
      "\n",
      "步骤 2 [AIMessage]：\n",
      "  调用工具：sql_db_list_tables\n",
      "  参数：{}\n",
      "  工具返回：employees, orders, products\n",
      "\n",
      "步骤 4 [AIMessage]：\n",
      "  调用工具：sql_db_schema\n",
      "  参数：{'table_names': 'employees'}\n",
      "  工具返回：\n",
      "CREATE TABLE employees (\n",
      "\tid INTEGER NOT NULL AUTO_INCREMENT, \n",
      "\tname VARCHAR(50) NOT NULL, \n",
      "\tdepartment VARCHAR(50) NOT NULL, \n",
      "\tsalary DECIMAL(10, 2) NOT NULL, \n",
      "\thire_date DATE, \n",
      "\tPRIMARY KEY (id)\n",
      ")E\n",
      "\n",
      "步骤 6 [AIMessage]：\n",
      "  调用工具：sql_db_query_checker\n",
      "  参数：{'query': 'SELECT department, COUNT(*) AS 人数 FROM employees GROUP BY department ORDER BY 人数 DESC'}\n",
      "  工具返回：SELECT department, COUNT(*) AS 人数 FROM employees GROUP BY department ORDER BY 人数 DESC\n",
      "\n",
      "步骤 8 [AIMessage]：\n",
      "  调用工具：sql_db_query\n",
      "  参数：{'query': 'SELECT department, COUNT(*) AS 人数 FROM employees GROUP BY department ORDER BY 人数 DESC'}\n",
      "  工具返回：[('技术部', 2), ('销售部', 2), ('人力资源', 1)]\n",
      "\n",
      "最终回答：查询结果如下：\n",
      "\n",
      "| 部门 | 人数 |\n",
      "|------|:----:|\n",
      "| 技术部 | 2 |\n",
      "| 销售部 | 2 |\n",
      "| 人力资源 | 1 |\n",
      "\n",
      "目前公司共有 **3 个部门**，总员工 **5 人**。其中技术部和销售部人数均为 2 人，人力资源部为 1 人，分布相对均衡。\n"
     ]
    }
   ],
   "source": [
    "response = sql_agent.invoke({\n",
    "    \"messages\": [{\"role\": \"user\", \"content\": \"每个部门各有多少人？\"}]\n",
    "})\n",
    "\n",
    "for i, msg in enumerate(response[\"messages\"]):\n",
    "    msg_type = msg.__class__.__name__\n",
    "\n",
    "    # AIMessage 中如果有 tool_calls，说明 Agent 调用了工具\n",
    "    if hasattr(msg, \"tool_calls\") and msg.tool_calls:\n",
    "        print(f\"\\n步骤 {i+1} [{msg_type}]：\")\n",
    "        for tc in msg.tool_calls:\n",
    "            print(f\"  调用工具：{tc['name']}\")\n",
    "            print(f\"  参数：{tc.get('args', {})}\")\n",
    "\n",
    "    # ToolMessage 是工具返回的结果\n",
    "    elif msg_type == \"ToolMessage\":\n",
    "        print(f\"  工具返回：{msg.content[:200]}\")\n",
    "\n",
    "    # AIMessage 的最终文本回答\n",
    "    elif msg.content:\n",
    "        print(f\"\\n最终回答：{msg.content}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "083faa70",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✅ 数据加载完成\n",
      "   员工表：5 行 × 5 列\n",
      "   产品表：4 行 × 5 列\n",
      "   订单表：5 行 × 5 列\n",
      "\n",
      "📋 员工表示例：\n",
      " id name department  salary  hire_date\n",
      "  1   张三        技术部 20000.0 2023-01-15\n",
      "  2   李四        销售部 11000.0 2023-02-20\n",
      "  3   王五        技术部 16000.0 2022-11-10\n"
     ]
    }
   ],
   "source": [
    "import matplotlib\n",
    "import matplotlib.font_manager as fm\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "import numpy as np\n",
    "# 1. 删除旧的字体缓存\n",
    "cache_dir = matplotlib.get_cachedir()\n",
    "for f in os.listdir(cache_dir):\n",
    "    if f.startswith('fontlist'):\n",
    "        os.remove(os.path.join(cache_dir, f))\n",
    "\n",
    "# 2. 强制重新扫描系统字体\n",
    "fm._load_fontmanager(try_read_cache=False)\n",
    "# ============================================================\n",
    "# 配置 matplotlib 中文显示\n",
    "# ============================================================\n",
    "# Windows 用 SimHei（黑体），macOS 用 PingFang SC\n",
    "# 如果还是乱码，试试安装 fonts-noto-cjk 并清除缓存\n",
    "plt.rcParams[\"font.sans-serif\"] = [\"SimHei\", \"PingFang SC\", \"DejaVu Sans\"]\n",
    "plt.rcParams[\"axes.unicode_minus\"] = False  # 解决负号显示为方块的问题\n",
    "\n",
    "# ============================================================\n",
    "# 从数据库加载数据到 Pandas DataFrame\n",
    "# ============================================================\n",
    "# 用 SQLAlchemy engine 复用连接，避免重复创建连接池\n",
    "from sqlalchemy import create_engine\n",
    "\n",
    "engine = create_engine(db_uri)\n",
    "\n",
    "employees_df = pd.read_sql(\"SELECT * FROM employees\", engine)\n",
    "products_df  = pd.read_sql(\"SELECT * FROM products\", engine)\n",
    "orders_df    = pd.read_sql(\"SELECT * FROM orders\", engine)\n",
    "\n",
    "print(\"✅ 数据加载完成\")\n",
    "print(f\"   员工表：{len(employees_df)} 行 × {len(employees_df.columns)} 列\")\n",
    "print(f\"   产品表：{len(products_df)} 行 × {len(products_df.columns)} 列\")\n",
    "print(f\"   订单表：{len(orders_df)} 行 × {len(orders_df.columns)} 列\")\n",
    "print(f\"\\n📋 员工表示例：\")\n",
    "print(employees_df.head(3).to_string(index=False))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "9bf4a205",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✅ Python 代码执行沙箱创建成功\n"
     ]
    }
   ],
   "source": [
    "import traceback\n",
    "from io import StringIO\n",
    "from contextlib import redirect_stdout\n",
    "from langchain.tools import tool\n",
    "\n",
    "# ============================================================\n",
    "# 定义沙箱的\"白名单\"——只有这些库和数据可以被代码访问\n",
    "# ============================================================\n",
    "# 这是一种简单的安全策略：不在白名单里的东西，代码碰不到\n",
    "SANDBOX_GLOBALS = {\n",
    "    # 数据：Agent 可以分析这三张表\n",
    "    \"employees_df\": employees_df,\n",
    "    \"products_df\":  products_df,\n",
    "    \"orders_df\":    orders_df,\n",
    "    # 工具库：Agent 可以用这些库做分析和画图\n",
    "    \"pd\":  pd,       # pandas — 数据处理\n",
    "    \"plt\": plt,      # matplotlib — 基础绑图\n",
    "    \"sns\": sns,      # seaborn — 统计可视化\n",
    "    \"np\":  np,       # numpy — 数值计算\n",
    "}\n",
    "\n",
    "\n",
    "@tool\n",
    "def execute_python_code(code: str) -> str:\n",
    "    \"\"\"\n",
    "    执行 Python 代码进行数据分析和可视化。\n",
    "\n",
    "    可用变量：\n",
    "      - employees_df: 员工表 DataFrame（字段：id, name, department, salary, hire_date）\n",
    "      - products_df:  产品表 DataFrame（字段：id, product_name, category, price, stock）\n",
    "      - orders_df:    订单表 DataFrame（字段：id, employee_id, product_id, quantity, order_date）\n",
    "      - pd:   pandas 库\n",
    "      - plt:  matplotlib.pyplot\n",
    "      - sns:  seaborn\n",
    "      - np:   numpy\n",
    "\n",
    "    使用示例：\n",
    "      # 统计各部门平均薪资\n",
    "      result = employees_df.groupby('department')['salary'].mean()\n",
    "      print(result)\n",
    "\n",
    "      # 画柱状图\n",
    "      plt.figure(figsize=(10, 6))\n",
    "      employees_df.groupby('department')['salary'].mean().plot(kind='bar')\n",
    "      plt.title('各部门平均薪资')\n",
    "      plt.show()\n",
    "    \"\"\"\n",
    "    # 准备隔离的执行环境\n",
    "    # globals_dict 提供白名单变量，locals_dict 收集执行过程中产生的新变量\n",
    "    exec_globals = dict(SANDBOX_GLOBALS)\n",
    "    exec_locals = {}\n",
    "\n",
    "    # 用 StringIO 捕获 print() 的输出\n",
    "    # 这样 Agent 生成的代码里所有的 print 语句都会被收集\n",
    "    output_buffer = StringIO()\n",
    "\n",
    "    try:\n",
    "        # redirect_stdout 会把标准输出重定向到我们的 buffer\n",
    "        with redirect_stdout(output_buffer):\n",
    "            exec(code, exec_globals, exec_locals)\n",
    "\n",
    "        result = output_buffer.getvalue()\n",
    "\n",
    "        # 如果代码没有 print 任何东西，给个默认提示\n",
    "        if not result.strip():\n",
    "            result = \"✅ 代码执行成功（无文本输出，可能已生成图表）\"\n",
    "\n",
    "        return f\"执行成功：\\n{result}\"\n",
    "\n",
    "    except Exception as e:\n",
    "        # 出错时返回完整的错误堆栈，方便 Agent 自我修正\n",
    "        error_detail = traceback.format_exc()\n",
    "        return f\"❌ 执行出错：{e}\\n\\n{error_detail}\"\n",
    "\n",
    "\n",
    "print(\"✅ Python 代码执行沙箱创建成功\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "bc6db1c3",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "数据可视化 Agent 创建完成\n"
     ]
    }
   ],
   "source": [
    "from langchain.agents import create_agent\n",
    "\n",
    "# ============================================================\n",
    "# 定义可视化 Agent 的 System Prompt\n",
    "# ============================================================\n",
    "VISUALIZATION_PROMPT = \"\"\"你是一名资深数据分析师，精通 Python、Pandas 和 Matplotlib 数据可视化。\n",
    "\n",
    "## 可用数据\n",
    "1. employees_df — 员工表（字段：id, name, department, salary, hire_date）\n",
    "2. products_df  — 产品表（字段：id, product_name, category, price, stock）\n",
    "3. orders_df    — 订单表（字段：id, employee_id, product_id, quantity, order_date）\n",
    "\n",
    "## 工作流程\n",
    "1. 理解用户的分析需求\n",
    "2. 用 execute_python_code 工具编写并执行 Python 代码\n",
    "3. 先做数据探索（head、describe、info），再做深入分析\n",
    "4. 用中文解释分析结果，给出业务洞察\n",
    "\n",
    "## 代码规范\n",
    "- 绑图前设置中文字体：plt.rcParams['font.sans-serif'] = ['SimHei', 'PingFang SC', 'DejaVu Sans']\n",
    "- 设置 plt.rcParams['axes.unicode_minus'] = False\n",
    "- 图表尺寸统一用 plt.figure(figsize=(10, 6))\n",
    "- 必须添加标题、坐标轴标签，让图表自解释\n",
    "- 用 print() 输出关键统计量，不要只画图不说话\n",
    "- 图表标题用英文（避免渲染问题），但用中文向用户解释结果\n",
    "\n",
    "## 注意事项\n",
    "- 每次只执行一段完整的代码，不要拆成多段\n",
    "- 先探索数据结构，再做分析——不要上来就画图\n",
    "- 结果要有业务洞察，不只是\"最大值是 XXX\"\n",
    "\"\"\"\n",
    "\n",
    "# ============================================================\n",
    "# 组装可视化 Agent（langchain 1.3.1 写法）\n",
    "# ============================================================\n",
    "viz_tools = [execute_python_code]\n",
    "\n",
    "# 同样用 create_agent 一行搞定，和 SQL Agent 的创建方式完全一致\n",
    "visualization_agent = create_agent(\n",
    "    model=llm,\n",
    "    tools=viz_tools,\n",
    "    system_prompt=VISUALIZATION_PROMPT,\n",
    ")\n",
    "\n",
    "print(\"数据可视化 Agent 创建完成\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "75e5e5c3",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 20656 characters]",
      "text/plain": [
       "<Figure size 1400x600 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 65356 characters]",
      "text/plain": [
       "<Figure size 1400x600 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<Figure size 1200x600 with 0 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 65356 characters]",
      "text/plain": [
       "<Figure size 1400x600 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 27788 characters]",
      "text/plain": [
       "<Figure size 1200x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "分析结果：\n",
      "好的，分析完成！以下是完整的 **员工薪资分布分析报告**：\n",
      "\n",
      "---\n",
      "\n",
      "## 📊 员工薪资分布分析报告\n",
      "\n",
      "### 一、数据概览\n",
      "数据集中共有 **5 名员工**，分布在 **3 个部门**（技术部、销售部、人力资源），薪资字段无缺失值。\n",
      "\n",
      "---\n",
      "\n",
      "### 二、关键统计指标\n",
      "\n",
      "| 指标 | 数值 |\n",
      "|------|------|\n",
      "| **平均薪资** | **13,800 元** |\n",
      "| **中位数薪资** | **16,000 元** |\n",
      "| **最低薪资** | **5,000 元** |\n",
      "| **最高薪资** | **20,000 元** |\n",
      "| **薪资范围** | **15,000 元** |\n",
      "| **标准差** | **5,890 元** |\n",
      "| **Q1（第一四分位）** | **11,000 元** |\n",
      "| **Q3（第三四分位）** | **17,000 元** |\n",
      "| **四分位距 (IQR)** | **6,000 元** |\n",
      "| **偏度** | **-0.86（左偏）** |\n",
      "\n",
      "---\n",
      "\n",
      "### 三、业务洞察\n",
      "\n",
      "**1️⃣ 薪资整体分布特征**\n",
      "- **均值（13,800）< 中位数（16,000）**，说明薪资分布呈现 **左偏（负偏）** 形态——即薪资较低端有极端值（人力资源的 5,000 元）拖低了均值，大部分人的薪资其实集中在 11,000~17,000 元区间。\n",
      "- 薪资范围宽达 **15,000 元**，最高薪资（20,000）是最低薪资（5,000）的 **4 倍**，反映出部门间薪资差距较大。\n",
      "\n",
      "**2️⃣ 部门薪资差异显著**\n",
      "| 部门 | 平均薪资 | 员工数 |\n",
      "|------|---------|-------|\n",
      "| 🔧 **技术部** | **18,000 元** | 2 人 |\n",
      "| 💼 **销售部** | **14,000 元** | 2 人 |\n",
      "| 👥 **人力资源** | **5,000 元** | 1 人 |\n",
      "\n",
      "- **技术部薪资最高**（平均 18,000 元），两名员工薪资分别为 16,000 和 20,000，内部差距 4,000 元。\n",
      "- **销售部居中**（平均 14,000 元），两人薪资为 11,000 和 17,000，内部差距达 6,000 元，销售岗位的业绩导向特征明显。\n",
      "- **人力资源最低**（仅 5,000 元），这个薪资明显低于其他部门，可能存在 **岗位价值评估偏低** 或 **数据不完整** 的情况，建议关注。\n",
      "\n",
      "**3️⃣ 箱线图分析**\n",
      "- 从箱线图看，数据中没有明显的异常值（所有点都在 1.5 倍 IQR 范围内）。\n",
      "- 但人力资源的 5,000 元薪资已接近下须边缘，属于 **合理范围内的最低值**，但仍需结合行业薪酬水平判断是否合理。\n",
      "\n",
      "---\n",
      "\n",
      "### 四、建议\n",
      "\n",
      "1. **人力资源薪资偏低**：5,000 元的薪资显著低于其他部门，建议对照市场薪酬水平进行复核，避免因薪资不公导致人才流失。\n",
      "2. **销售部薪资差距大**（11,000~17,000），建议明确绩效薪资结构，确保内部公平性。\n",
      "3. **数据量较少**（仅 5 条），以上结论受数据规模限制，建议收集更多员工数据以获得更可靠的薪资分布洞察。\n"
     ]
    }
   ],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings(\"ignore\")  # 抑制 matplotlib 的字体警告\n",
    "\n",
    "response = visualization_agent.invoke({\n",
    "    \"messages\": [{\"role\": \"user\", \"content\": \"分析一下员工薪资的分布情况，包括均值、中位数、最大最小值等统计量\"}]\n",
    "})\n",
    "final_msg = response[\"messages\"][-1]\n",
    "print(f\"\\n分析结果：\\n{final_msg.content}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "515bfde9",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"=== 薪资统计概览 ===\")\n",
    "print(employees_df['salary'].describe())\n",
    "\n",
    "# 计算额外统计量\n",
    "median_salary = employees_df['salary'].median()\n",
    "print(f\"\\n中位数薪资：{median_salary:,.0f} 元\")\n",
    "print(f\"薪资范围：{employees_df['salary'].min():,.0f} ~ {employees_df['salary'].max():,.0f} 元\")\n",
    "print(f\"薪资标准差：{employees_df['salary'].std():,.0f} 元\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "58b8aca8",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 定制化图表内容\n",
    "plt.rcParams['font.sans-serif'] = ['SimHei', 'PingFang SC', 'DejaVu Sans']\n",
    "plt.rcParams['axes.unicode_minus'] = False\n",
    "\n",
    "# 按部门计算平均薪资\n",
    "dept_salary = employees_df.groupby('department')['salary'].mean().sort_values(ascending=False)\n",
    "\n",
    "# 画柱状图\n",
    "plt.figure(figsize=(10, 6))\n",
    "bars = plt.bar(dept_salary.index, dept_salary.values, color=['#4C78A8', '#F58518', '#E45756'])\n",
    "plt.title('Average Salary by Department', fontsize=16)\n",
    "plt.xlabel('Department', fontsize=12)\n",
    "plt.ylabel('Average Salary (CNY)', fontsize=12)\n",
    "\n",
    "# 在柱子上方标注具体数值\n",
    "for bar in bars:\n",
    "    height = bar.get_height()\n",
    "    plt.text(bar.get_x() + bar.get_width()/2., height,\n",
    "             f'{height:,.0f}', ha='center', va='bottom', fontsize=11)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eff33063",
   "metadata": {},
   "outputs": [],
   "source": [
    "response = visualization_agent.invoke({\n",
    "    \"messages\": [{\"role\": \"user\", \"content\": \"计算每个产品类别的库存总量，用水平条形图展示，并标注具体数值\"}]\n",
    "})\n",
    "final_msg = response[\"messages\"][-1]\n",
    "print(f\"\\n分析结果：\\n{final_msg.content}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1c4d1ca1",
   "metadata": {},
   "outputs": [],
   "source": [
    "response = visualization_agent.invoke({\n",
    "    \"messages\": [{\"role\": \"user\", \"content\": \"分析各产品类别的平均价格，画一个饼图\"}]\n",
    "})\n",
    "\n",
    "for i, msg in enumerate(response[\"messages\"]):\n",
    "    msg_type = msg.__class__.__name__\n",
    "\n",
    "    # AIMessage 中的 tool_calls 包含 Agent 生成的代码\n",
    "    if msg_type == \"AIMessage\" and hasattr(msg, \"tool_calls\") and msg.tool_calls:\n",
    "        print(f\"\\n{'='*60}\")\n",
    "        for tc in msg.tool_calls:\n",
    "            if tc[\"name\"] == \"execute_python_code\":\n",
    "                print(f\"Agent 生成的第 {i+1} 段代码：\")\n",
    "                print(f\"{'='*60}\")\n",
    "                print(tc[\"args\"].get(\"code\", \"\"))\n",
    "\n",
    "    # ToolMessage 是代码执行的结果\n",
    "    elif msg_type == \"ToolMessage\":\n",
    "        print(f\"\\n--- 执行结果 ---\")\n",
    "        print(msg.content[:500])\n",
    "\n",
    "# 最终回答\n",
    "final_msg = response[\"messages\"][-1]\n",
    "print(f\"\\n{'='*60}\")\n",
    "print(f\"最终回答：\\n{final_msg.content}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "df23159e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 编排器：根据用户意图路由到合适的 Agent\n",
    "def run_data_analysis(user_query: str) -> str:\n",
    "    \"\"\"\n",
    "    统一入口：自动判断用户意图，路由到对应的 Agent。\n",
    "    \"\"\"\n",
    "    # 用 LLM 做意图分类（也可以用规则匹配，看场景复杂度）\n",
    "    classification_prompt = f\"\"\"判断以下用户问题属于哪种类型：\n",
    "    - \"query\"：需要查询数据库获取数据\n",
    "    - \"visualize\"：需要画图或做统计分析\n",
    "    - \"both\"：需要先查数据，再画图分析\n",
    "\n",
    "    用户问题：{user_query}\n",
    "\n",
    "    只回复一个词：query / visualize / both\"\"\"\n",
    "\n",
    "    intent = llm.invoke(classification_prompt).content.strip().lower()\n",
    "\n",
    "    if intent == \"query\":\n",
    "        # create_agent 返回的消息格式：取最后一条消息\n",
    "        result = sql_agent.invoke({\"messages\": [{\"role\": \"user\", \"content\": user_query}]})\n",
    "        return result[\"messages\"][-1].content\n",
    "    elif intent == \"visualize\":\n",
    "        result = visualization_agent.invoke({\"messages\": [{\"role\": \"user\", \"content\": user_query}]})\n",
    "        return result[\"messages\"][-1].content\n",
    "    else:  # both\n",
    "        # 先查数据\n",
    "        data_result = sql_agent.invoke({\"messages\": [{\"role\": \"user\", \"content\": user_query}]})\n",
    "        data_content = data_result[\"messages\"][-1].content\n",
    "        # 把查询结果传给可视化 Agent\n",
    "        viz_input = f\"基于以下数据进行可视化分析：\\n{data_content}\\n\\n原始问题：{user_query}\"\n",
    "        viz_result = visualization_agent.invoke({\"messages\": [{\"role\": \"user\", \"content\": viz_input}]})\n",
    "        return viz_result[\"messages\"][-1].content"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "66564354",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "双Agent准备完成\n"
     ]
    }
   ],
   "source": [
    "# 把 SQL 工具和 Python 代码执行工具都挂到同一个 Agent 上，\n",
    "# 让 LLM 自己决定调用哪些工具。优点是简单，缺点是工具太多时 LLM 容易\"选择困难\"。\n",
    "both_tools = tools + viz_tools\n",
    "\n",
    "BOTH_PROMPT = \"\"\"你是一命资深数据分析师，精通SQL查询数据以及Python、Pandas 和 Matplotlib 制作数据可视化图表。\n",
    "\n",
    "## 可用数据\n",
    "1. employees_df — 员工表（字段：id, name, department, salary, hire_date）\n",
    "2. products_df  — 产品表（字段：id, product_name, category, price, stock）\n",
    "3. orders_df    — 订单表（字段：id, employee_id, product_id, quantity, order_date）\n",
    "\n",
    "## 工作流程\n",
    "1. 优先判断用户需求是关于“SQL”查询还是“生出可视化图表”，确定好合适的需求后调用工具帮助用户解决问题\n",
    "2. 如果需求是是SQL查询，例如想要查询数据库数据，工作流程如下:\n",
    "    (1) 先用 sql_db_list_tables 查看数据库中有哪些表\n",
    "    (2) 用 sql_db_schema 获取相关表的字段结构和类型\n",
    "    (3) 生成 SQL 之前，用 sql_db_query_checker 检查语法\n",
    "    (4) 确认无误后，用 sql_db_query 执行查询\n",
    "    (5) 用中文总结查询结果，给出简洁的业务洞察\n",
    "3. 如果需求是生成图表或做统计分析，工作流程如下：\n",
    "    (1) 理解用户的分析需求\n",
    "    (2) 用 execute_python_code 工具编写并执行 Python 代码\n",
    "    (3) 先做数据探索（head、describe、info），再做深入分析\n",
    "    (4) 用中文解释分析结果，给出业务洞察\n",
    "4. 如果需要同时执行SQL查询和统计分析，按照先查询SQL(回到步骤2),再生成统计图表(回到步骤3的顺序执行)\n",
    "5. 如果用户的提问和SQL查询、统计数据、图表生成等问题无关，或者询问的内容不属于员工表、产品表、订单表的信息，请用户再次明确问题，并告知无法查询到相关信息\n",
    "\n",
    "## 约束\n",
    "如果执行SQL查询，约束如下：\n",
    "- 只使用数据库中实际存在的表和字段，不要凭空编造\n",
    "- 单次查询结果限制在 50 条以内\n",
    "- 如果查询出错，分析错误原因后重新生成 SQL\n",
    "- 回答要简洁专业，不要啰嗦\n",
    "如果执行生成图表，约束如下：\n",
    "- 每次只执行一段完整的代码，不要拆成多段\n",
    "- 先探索数据结构，再做分析——不要上来就画图\n",
    "- 结果要有业务洞察，不只是\"最大值是 XXX\"\n",
    "\n",
    "## 代码规范\n",
    "- 绑图前设置中文字体：plt.rcParams['font.sans-serif'] = ['SimHei', 'PingFang SC', 'DejaVu Sans']\n",
    "- 设置 plt.rcParams['axes.unicode_minus'] = False\n",
    "- 图表尺寸统一用 plt.figure(figsize=(10, 6))\n",
    "- 必须添加标题、坐标轴标签，让图表自解释\n",
    "- 用 print() 输出关键统计量，不要只画图不说话\n",
    "- 图表标题用英文（避免渲染问题），但用中文向用户解释结果\n",
    "\n",
    "## 注意事项\n",
    "\"\"\"\n",
    "both_agent = create_agent(\n",
    "    model=llm,\n",
    "    tools=both_tools,\n",
    "    system_prompt=BOTH_PROMPT,\n",
    ")\n",
    "\n",
    "print(\"双Agent准备完成\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "99a92ac2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "回答：查询完成！以下是 **技术部** 所有员工的姓名和薪资（按薪资从高到低排序）：\n",
      "\n",
      "| 姓名 | 薪资 |\n",
      "|------|------|\n",
      "| 张三 | ¥20,000.00 |\n",
      "| 王五 | ¥16,000.00 |\n",
      "\n",
      "**业务洞察：**\n",
      "- 技术部共有 **2 名员工**，平均薪资为 **¥18,000.00**。\n",
      "- 薪资最高的是 **张三（¥20,000）**，比第二名王五高出 **¥4,000（25%）**，说明技术部内部薪资存在一定差距。\n"
     ]
    }
   ],
   "source": [
    "response = both_agent.invoke({\n",
    "    \"messages\": [{\"role\": \"user\", \"content\": \"列出技术部所有员工的姓名和薪资，按薪资从高到低排序\"}]\n",
    "})\n",
    "final_msg = response[\"messages\"][-1]\n",
    "print(f\"回答：{final_msg.content}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "197be8a6",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "findfont: Failed to find font weight bold, now using 400.\n",
      "findfont: Failed to find font weight bold, now using 400.\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 33416 characters]",
      "text/plain": [
       "<Figure size 1000x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "分析结果：\n",
      "## 分析结果与业务洞察\n",
      "\n",
      "### 📊 各产品类别库存总量\n",
      "\n",
      "| 产品类别 | 库存总量 |\n",
      "|---------|:--------:|\n",
      "| 🖥️ **电子产品** | **1,900 件** |\n",
      "| 🪑 **办公用品** | **300 件** |\n",
      "| **合计** | **2,200 件** |\n",
      "\n",
      "### 💡 业务洞察\n",
      "\n",
      "1. **电子产品占据绝对主导**——库存占比高达 **86.4%**（1,900/2,200），是办公用品的 **6 倍多**，说明该公司核心业务聚焦在电子产品线。\n",
      "\n",
      "2. **办公用品库存偏少**——仅 300 件，可能因为该类目产品较少（只有 1 款办公椅），建议关注是否有扩展办公用品品类的空间。\n",
      "\n",
      "3. **库存结构不均衡**——两个类别的库存量差距悬殊，建议进一步分析销售周转率，判断电子产品是否存在**库存积压**风险，而办公用品是否存在**断货隐患**。\n",
      "\n",
      "4. **整体库存水位**——总库存 2,200 件，结合订单表数据可以进一步评估库存周转效率，判断是否需要优化采购策略。\n",
      "\n",
      "> 📌 如需深入分析，可以进一步查看各产品类别的销量情况，计算**库存周转天数**，更精准地评估健康度。\n"
     ]
    }
   ],
   "source": [
    "response = visualization_agent.invoke({\n",
    "    \"messages\": [{\"role\": \"user\", \"content\": \"计算每个产品类别的库存总量，用水平条形图展示，并标注具体数值\"}]\n",
    "})\n",
    "final_msg = response[\"messages\"][-1]\n",
    "print(f\"\\n分析结果：\\n{final_msg.content}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "8da88f92",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "回答：抱歉，当前数据库中只有以下三张表：\n",
      "\n",
      "1. **employees** — 员工表\n",
      "2. **products** — 产品表\n",
      "3. **orders** — 订单表\n",
      "\n",
      "**没有「学生表」**，因此无法查询学生的姓名、学号和分数信息。\n",
      "\n",
      "请您重新明确需求，例如：\n",
      "- 想查询**员工**的姓名和相关信息？\n",
      "- 想查询**产品**或**订单**的数据？\n",
      "- 或者有其他分析需求？\n",
      "\n",
      "确认后我来帮您处理！\n"
     ]
    }
   ],
   "source": [
    "response = both_agent.invoke({\n",
    "    \"messages\": [{\"role\": \"user\", \"content\": \"列出所有学生的姓名和学号，按分数从高到低排序\"}]\n",
    "})\n",
    "final_msg = response[\"messages\"][-1]\n",
    "print(f\"回答：{final_msg.content}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "02_DATA_Analysis",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
