{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "18e71e02",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "北京今天是晴天，气温 32°C，湿度 45%，天气不错，很适合出行哦！\n",
      "\n",
      "另外帮你算好啦，356 × 128 的结果是 **45568**。\n"
     ]
    }
   ],
   "source": [
    "import os\n",
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.tools import tool\n",
    "from langchain.agents import create_agent\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "load_dotenv()\n",
    "\n",
    "\n",
    "model = ChatOpenAI(\n",
    "    model=os.getenv('QWEN_MODEL'),\n",
    "    api_key=os.getenv('QWEN_API_KEY'),\n",
    "    base_url=os.getenv('QWEN_API_BASE')\n",
    ")\n",
    "\n",
    "# 第二步。定义工具 （只有两个，一清二楚）\n",
    "\n",
    "@tool\n",
    "def get_weather(city: str) -> str:\n",
    "    \"\"\"查询指定城市的实时天气。\n",
    "\n",
    "    当用户问\"某地天气怎么样\"、\"热不热\"、\"要不要带伞\"时调用。\n",
    "\n",
    "    Args:\n",
    "        city: 城市名称，例如\"北京\"、\"上海\"、\"深圳\"\n",
    "    \"\"\"\n",
    "    # 模拟天气数据（实际项目中对接真实天气API）\n",
    "    weather_db = {\n",
    "        \"北京\": \"☀️ 晴天，32°C，湿度 45%，适合出行\",\n",
    "        \"上海\": \"🌧️ 小雨，28°C，湿度 80%，建议带伞\",\n",
    "        \"深圳\": \"⛅ 多云，30°C，湿度 65%\",\n",
    "    }       \n",
    "    return weather_db.get(city, f\"抱歉，我无法查询到 {city} 的天气信息\")\n",
    "\n",
    "@tool\n",
    "def calculator(expression: str) -> str:\n",
    "    \"\"\"执行数学计算，支持加减乘除和括号。\n",
    "\n",
    "    当用户说\"帮我算\"、\"等于多少\"、\"计算一下\"时调用。\n",
    "\n",
    "    Args:\n",
    "        expression: 数学表达式，例如 \"128 * 456\"、\"2 ** 10\"、\"(88 + 12) / 5\"\n",
    "    \"\"\"\n",
    "    try:\n",
    "        result = eval(expression)\n",
    "        return f\"🧮 {expression} = {result}\"\n",
    "    except Exception as e:\n",
    "        return f\"❌ 计算出错：{e}\"\n",
    "\n",
    "# 第三步：组装 Agent\n",
    "# create_agent 内部自动构建了 Think→Act→Observe 循环\n",
    "\n",
    "tools = [get_weather, calculator]\n",
    "\n",
    "agent = create_agent(\n",
    "    model=model,\n",
    "    tools=tools,\n",
    "    system_prompt=(\n",
    "        \"你是一个热心肠的生活助手，会查天气、会做算术。\\n\"\n",
    "        \"规则：\\n\"\n",
    "        \"1. 用户问天气 → 调用 get_weather。\\n\"\n",
    "        \"2. 用户要计算 → 调用 calculator。\\n\"\n",
    "        \"3. 一个请求涉及多个工具时，逐个调用，每次观察结果后再决定下一步。\\n\"\n",
    "        \"4. 闲聊类的问候直接回答，不用调工具。\"\n",
    "    ),\n",
    ")\n",
    "\n",
    "# 第四步：测试 Agent\n",
    "\n",
    "response = agent.invoke({\n",
    "    \"messages\": [\n",
    "        {\"role\": \"user\", \"content\": \"北京今天天气怎么样？顺便帮我算一下 356 × 128\"}\n",
    "    ]\n",
    "})\n",
    "\n",
    "print(response[\"messages\"][-1].content)\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "dc9c17e9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 11184 characters]",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import Image, display\n",
    "\n",
    "# draw_mermaid_png() 生成 Mermaid 格式的流程图 PNG\n",
    "display(Image(agent.get_graph().draw_mermaid_png()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "5f56431f",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import operator\n",
    "from typing import Annotated, List, Tuple, TypedDict, Union, Literal\n",
    "\n",
    "from langchain_community.tools.tavily_search import TavilySearchResults\n",
    "from langchain_community.chat_models import ChatTongyi\n",
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "from langchain.agents import create_agent\n",
    "from pydantic import BaseModel, Field\n",
    "\n",
    "#https://tavily.com/\n",
    "#uv pip install tavily-python -i https://pypi.tuna.tsinghua.edu.cn/simple\n",
    "\n",
    "# ---- 配置区：请替换为你自己的 Key ----\n",
    "os.environ[\"TAVILY_API_KEY\"] = os.getenv('TAVILY_API_KEY')\n",
    "DASHSCOPE_API_KEY = os.getenv('QWEN_API_KEY')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "5f1f924c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 初始化搜索工具，max_results 控制每次搜索返回的条数\n",
    "search_tool = TavilySearchResults(max_results=3)\n",
    "tools = [search_tool]\n",
    "\n",
    "# 规划阶段用推理能力强的 Qwen-Max\n",
    "planner_llm = ChatTongyi(\n",
    "    model=\"qwen-max\",\n",
    "    temperature=0.1,          # 低温度保证规划稳定\n",
    "    api_key=DASHSCOPE_API_KEY,\n",
    ")\n",
    "\n",
    "# 执行阶段可以用更经济的模型，这里用 Qwen-Plus\n",
    "executor_llm = ChatTongyi(\n",
    "    model=\"qwen-plus\",\n",
    "    temperature=0.01,\n",
    "    api_key=DASHSCOPE_API_KEY,\n",
    ")\n",
    "\n",
    "# 创建执行 Agent—— 它就是一个标准的 ReAct Agent\n",
    "executor_agent = create_agent(\n",
    "    model=executor_llm,\n",
    "    tools=tools,\n",
    "    system_prompt=\"你是一个执行力很强的助手。请根据给定的任务步骤，准确完成并给出结果。\",\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "3333d5a8",
   "metadata": {},
   "outputs": [],
   "source": [
    "class PlanExecute(TypedDict):\n",
    "    \"\"\"贯穿整个工作流的状态对象\"\"\"\n",
    "    input: str                                              # 用户原始输入\n",
    "    plan: List[str]                                         # 当前计划步骤列表\n",
    "    # past_steps 用 operator.add 做累加——每次节点返回新步骤时自动追加到历史\n",
    "    past_steps: Annotated[List[Tuple[str, str]], operator.add]\n",
    "    response: str                                           # 最终回复（非空时流程结束）\n",
    "\n",
    "\n",
    "class Plan(BaseModel):\n",
    "    \"\"\"规划器输出的结构化计划\"\"\"\n",
    "    steps: List[str] = Field(\n",
    "        description=\"需要顺序执行的步骤列表，每一步必须独立且包含完整上下文\"\n",
    "    )\n",
    "\n",
    "\n",
    "class Response(BaseModel):\n",
    "    \"\"\"直接回复用户的内容\"\"\"\n",
    "    response: str\n",
    "\n",
    "\n",
    "class Act(BaseModel):\n",
    "    \"\"\"\n",
    "    重规划器的决策输出——要么回复用户，要么给出更新后的计划。\n",
    "    用 Union 类型确保一次只走一条分支。\n",
    "    \"\"\"\n",
    "    action: Union[Response, Plan] = Field(\n",
    "        description=\"如果任务已完成，返回 Response；如果还需要继续，返回 Plan\"\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "bb88d898",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "  Step 1: 首先，访问通义灵码、文心快码、CodeGeeX的官方网站或权威评测网站来收集关于这三个代码助手工具在2025年的最新信息。\n",
      "  Step 2: 记录每个工具支持的编程语言种类，并制作一份列表以便后续比较。\n",
      "  Step 3: 查找有关这三个工具在2025年时对于不同编程语言的代码补全准确率的数据或用户反馈，整理成对比表。\n",
      "  Step 4: 调查并记录这些工具的价格策略，包括但不限于订阅费用、是否有免费试用版等，并将其汇总到价格比较表中。\n",
      "  Step 5: 基于收集到的支持语言范围、代码补全准确率以及价格三个维度的信息，分析每个工具的优势与劣势，并据此提出针对不同需求场景下的选型建议。\n"
     ]
    }
   ],
   "source": [
    "# 规划器的系统提示——核心是\"拆得合理、不越界\"\n",
    "planner_prompt = ChatPromptTemplate.from_messages([\n",
    "    (\n",
    "        \"system\",\n",
    "        \"\"\"你是一个擅长任务分解的规划专家。对于用户提出的目标，请将其拆解为顺序执行的步骤列表。\n",
    "\n",
    "规则：\n",
    "1. 每一步必须自包含——执行者不需要上下文就能理解这步要做什么\n",
    "2. 不要添加无关步骤，最后一步的结果应该直接导向最终答案\n",
    "3. 优先使用中文描述步骤\n",
    "4. 步骤数量控制在 3~6 步，过少说明拆分不到位，过多说明混入了无关操作\"\"\",\n",
    "    ),\n",
    "    (\"placeholder\", \"{messages}\"),\n",
    "])\n",
    "\n",
    "# 将提示词模板和 LLM 拼成一条链，with_structured_output 让 LLM 直接输出 Plan 对象\n",
    "planner_chain = planner_prompt | planner_llm.with_structured_output(Plan)\n",
    "\n",
    "\n",
    "def plan_step(state: PlanExecute) -> dict:\n",
    "    \"\"\"规划节点：分析用户原始输入，生成执行计划\"\"\"\n",
    "    result = planner_chain.invoke({\"messages\": [(\"user\", state[\"input\"])]})\n",
    "    return {\"plan\": result.steps}\n",
    "result = planner_chain.invoke({\n",
    "    \"messages\": [(\n",
    "        \"user\",\n",
    "        \"帮我调研一下2025年国内主流代码助手工具（通义灵码、文心快码、CodeGeeX），\"\n",
    "        \"从代码补全准确率、支持语言、价格三个维度做对比，最后给出选型建议。\"\n",
    "    )]\n",
    "})\n",
    "\n",
    "for i, step in enumerate(result.steps, 1):\n",
    "    print(f\"  Step {i}: {step}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "8079dc6f",
   "metadata": {},
   "outputs": [],
   "source": [
    "def execute_step(state: PlanExecute) -> dict:\n",
    "    \"\"\"\n",
    "    从当前计划中取出第一步，交给执行 Agent 去跑。\n",
    "    执行完后把 (步骤描述, 执行结果) 追加到 past_steps。\n",
    "    \"\"\"\n",
    "    plan = state[\"plan\"]\n",
    "\n",
    "    # 把完整计划格式化，让 Agent 知道\"我在做什么、后面还有什么\"\n",
    "    plan_overview = \"\\n\".join(f\"  {i+1}. {step}\" for i, step in enumerate(plan))\n",
    "    current_task = plan[0]  # 只取第一步\n",
    "\n",
    "    # 拼装给 Agent 的指令：先展示全貌，再指定当前任务\n",
    "    task_prompt = f\"\"\"以下是完整的执行计划：\n",
    "{plan_overview}\n",
    "\n",
    "现在请你只执行第 1 步，不要做后续步骤：\n",
    "  → {current_task}\n",
    "\n",
    "请直接给出这一步的执行结果，不要啰嗦。\"\"\"\n",
    "\n",
    "    agent_result = executor_agent.invoke({\n",
    "        \"messages\": [(\"user\", task_prompt)]\n",
    "    })\n",
    "\n",
    "    # 取 Agent 最后一条消息的内容作为这一步的产出\n",
    "    step_output = agent_result[\"messages\"][-1].content\n",
    "\n",
    "    return {\n",
    "        # 这一步走完后，计划需要弹出已执行的第一项——这个逻辑在 replan_step 里\n",
    "        \"past_steps\": [(current_task, step_output)],\n",
    "    }"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "d1b0c1c3",
   "metadata": {},
   "outputs": [],
   "source": [
    "replanner_prompt = ChatPromptTemplate.from_template(\"\"\"\n",
    "你是一个项目进度管控专家。根据以下信息，判断当前任务的状态并做出决策。\n",
    "\n",
    "## 用户原始目标\n",
    "{input}\n",
    "\n",
    "## 原始计划\n",
    "{plan}\n",
    "\n",
    "## 已完成的步骤及结果\n",
    "{past_steps}\n",
    "\n",
    "## 决策规则\n",
    "1. 如果所有步骤已完成且结果充分，直接给出最终答案（使用 Response）\n",
    "2. 如果还有步骤未执行，返回更新后的计划（使用 Plan），注意：\n",
    "   - 只保留尚未执行的步骤\n",
    "   - 如果已完成步骤的结果表明原计划的后续步骤需要调整，直接修改\n",
    "   - 不要返回已经做过的步骤\n",
    "3. 如果某个步骤执行失败了，请在计划中补充重试或替代方案\n",
    "\"\"\")\n",
    "\n",
    "replanner_chain = replanner_prompt | planner_llm.with_structured_output(Act)\n",
    "\n",
    "\n",
    "def replan_step(state: PlanExecute) -> dict:\n",
    "    \"\"\"审视执行进度，决定下一步走向\"\"\"\n",
    "    try:\n",
    "        output = replanner_chain.invoke(state)\n",
    "    except Exception:\n",
    "        # structured output 解析失败时的兜底方案：\n",
    "        # 用普通 LLM 调用直接生成最终回复，避免 JSON 格式问题导致整个流程崩溃\n",
    "        fallback_prompt = ChatPromptTemplate.from_template(\"\"\"\n",
    "## 用户原始目标\n",
    "{input}\n",
    "\n",
    "## 已完成的步骤及结果\n",
    "{past_steps}\n",
    "\n",
    "所有步骤已完成，请根据以上收集到的信息，用中文为用户输出一份完整的调研对比报告和选型建议。\n",
    "\"\"\")\n",
    "        fallback_chain = fallback_prompt | planner_llm\n",
    "        fallback_result = fallback_chain.invoke({\n",
    "            \"input\": state[\"input\"],\n",
    "            \"past_steps\": state[\"past_steps\"],\n",
    "        })\n",
    "        return {\"response\": fallback_result.content}\n",
    "\n",
    "    if isinstance(output.action, Response):\n",
    "        # 任务完成，直接返回给用户\n",
    "        return {\"response\": output.action.response}\n",
    "    else:\n",
    "        # 还有步骤要执行，更新计划列表\n",
    "        return {\"plan\": output.action.steps}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "eaf11437",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.graph import StateGraph, START, END\n",
    "\n",
    "# ---- 第一步：创建状态图，绑定状态类型 ----\n",
    "workflow = StateGraph(PlanExecute)\n",
    "\n",
    "# ---- 第二步：注册三个核心节点 ----\n",
    "workflow.add_node(\"planner\", plan_step)       # 规划节点\n",
    "workflow.add_node(\"executor\", execute_step)   # 执行节点\n",
    "workflow.add_node(\"replanner\", replan_step)   # 重规划节点\n",
    "\n",
    "# ---- 第三步：编排边的流向 ----\n",
    "workflow.add_edge(START, \"planner\")           # 入口 → 规划\n",
    "workflow.add_edge(\"planner\", \"executor\")      # 规划 → 执行\n",
    "workflow.add_edge(\"executor\", \"replanner\")    # 执行 → 重规划\n",
    "\n",
    "# ---- 第四步：条件分支——重规划后走哪条路 ----\n",
    "def should_continue(state: PlanExecute) -> Literal[\"executor\", \"__end__\"]:\n",
    "    \"\"\"如果 response 字段非空，说明重规划器认为任务完成，结束流程\"\"\"\n",
    "    if state.get(\"response\"):\n",
    "        return END\n",
    "    return \"executor\"\n",
    "\n",
    "workflow.add_conditional_edges(\"replanner\", should_continue, {\n",
    "    \"executor\": \"executor\",\n",
    "    \"__end__\": END,\n",
    "})\n",
    "\n",
    "# ---- 编译 ----\n",
    "app = workflow.compile()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "27d36ada",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 25344 characters]",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import Image, display\n",
    "\n",
    "# xray=True 会展示更多内部细节\n",
    "display(Image(app.get_graph(xray=True).draw_mermaid_png()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "9addf53e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "==================================================\n",
      "📍 当前节点: planner\n",
      "  plan: ['调查通义灵码在2025年的代码补全准确率、支持的编程语言种类以及使用价格。', '调查文心快码在2025年的代码补全准确率、支持的编程语言种类以及使用价格。', '调查CodeGeeX在2025年的代码补全准确率、支持的编程语言种类以及使用价格。', '根据收集到的信息，从代码补全准确率、支持的语言范围和价格三个维度对比这三个工具。', '基于对比结果，提出对于不同需求用户（如个人开发者、小型团队、大型企业）的选型建议。']\n",
      "\n",
      "==================================================\n",
      "📍 当前节点: executor\n",
      "  past_steps: [('调查通义灵码在2025年的代码补全准确率、支持的编程语言种类以及使用价格。', '根据最新公开信息（截至2025年），通义灵码：\\n\\n- **代码补全准确率**：未公布具体百分比数值；但官方强调其基于Qwen3-Coder模型（235B总参，22B激活），在SecCodeBench安全基准测试中生成代码安全性全球第一，Java万行漏洞数降低61%，且在主流编程语言AI代码生成占比超30%，表明其上下文理解与生成准确性处于行业领先水平。\\n\\n- **支持的编程语言种类**：官方明确支持**超过130种编程语言**，涵盖Java、Python、Go、TypeScript、JavaScript、C/C++、Rust、SQL、Shell等主流及小众语言，并采用AST感知编码、跨语言对齐等技术增强多语言适配能力。\\n\\n- **使用价格**：  \\n  - **个人免费版**：基础功能（含行/函数级补全、智能问答、单元测试生成等）完全免费，支持阿里云国际站账号登录；  \\n  - **企业版**：提供标准版与专属版（支持VPC私有部署、多模型配置、MCP工具集成等），按席位/算力/地域订阅计费，具体定价需联系阿里云销售；  \\n  - 无公开统一标价，但2025年4月更新强调Qwen3模型“成本大幅下降”，暗示企业版性价比提升。')]\n",
      "\n",
      "==================================================\n",
      "📍 当前节点: replanner\n",
      "  plan: ['调查文心快码在2025年的代码补全准确率、支持的编程语言种类以及使用价格。', '调查CodeGeeX在2025年的代码补全准确率、支持的编程语言种类以及使用价格。', '根据收集到的信息，从代码补全准确率、支持的语言范围和价格三个维度对比这三个工具。', '基于对比结果，提出对于不同需求用户（如个人开发者、小型团队、大型企业）的选型建议。']\n",
      "\n",
      "==================================================\n",
      "📍 当前节点: executor\n",
      "  past_steps: [('调查文心快码在2025年的代码补全准确率、支持的编程语言种类以及使用价格。', '根据搜索结果，截至2025年：\\n\\n- **代码补全准确率**：未在公开资料中披露具体数值（如百分比或基准测试得分），但IDC评测指出其“位居国内代码生成产品评估第一”，且实测显示其在中文语境、跨文件/工程级上下文理解（RAG）、业务逻辑调用（如自动识别并调用 `UserScoreService`）等方面显著优于竞品，表明实际补全准确率处于行业领先水平。\\n\\n- **支持的编程语言种类**：明确支持 **100+ 种主流语言**，并对国内高频使用的 **Java（Spring Cloud）、C/C++、Go** 进行了深度微调；特别强调对中文注释、中文需求文档及拼音缩写变量的原生理解能力。\\n\\n- **使用价格**：官网（comate.baidu.com）仅展示“企业版”与“个人旗舰版”入口，未公开具体定价；搜索结果中无权威来源披露2025年标准报价（如按用户/月、私有化部署License费等），亦无免费版或基础版价格说明。需联系销售获取定制报价。\\n\\n综上：准确率无量化数据但综合能力获IDC认证第一；语言支持超100种，侧重中文生态与主流企业语言；价格未公开，属商业询价模式。')]\n",
      "\n",
      "==================================================\n",
      "📍 当前节点: replanner\n",
      "  plan: ['调查CodeGeeX在2025年的代码补全准确率、支持的编程语言种类以及使用价格。', '根据收集到的信息，从代码补全准确率、支持的语言范围和价格三个维度对比这三个工具。', '基于对比结果，提出对于不同需求用户（如个人开发者、小型团队、大型企业）的选型建议。']\n",
      "\n",
      "==================================================\n",
      "📍 当前节点: executor\n",
      "  past_steps: [('调查CodeGeeX在2025年的代码补全准确率、支持的编程语言种类以及使用价格。', '- **代码补全准确率**：CodeGeeX在2025年公开报告的HumanEval基准测试得分为**82.3%**（来源：Augment Code对比报告，2025年9月更新）。  \\n- **支持的编程语言种类**：明确支持Python、Java、C++、JavaScript、Go、Rust等主流语言，以及**14+种额外语言**（共超20种），具备跨语言翻译能力（来源：CheckThat.ai产品页）。  \\n- **使用价格**：**完全免费且开源**，支持本地/私有化部署；无官方商业订阅计划，亦无面向个人或企业的付费版本披露（多个信源均未提及收费模式，强调“open-source release”和“self-hosting options”）。')]\n",
      "\n",
      "==================================================\n",
      "📍 当前节点: replanner\n",
      "  plan: ['根据收集到的信息，从代码补全准确率、支持的语言范围和价格三个维度对比这三个工具。', '基于对比结果，提出对于不同需求用户（如个人开发者、小型团队、大型企业）的选型建议。']\n",
      "\n",
      "==================================================\n",
      "📍 当前节点: executor\n",
      "  past_steps: [('根据收集到的信息，从代码补全准确率、支持的语言范围和价格三个维度对比这三个工具。', '| 维度 | GitHub Copilot | Tabnine | Amazon CodeWhisperer |\\n|------|----------------|---------|------------------------|\\n| **代码补全准确率** | 最高，整体准确率优秀，覆盖多样化编程场景；在AWS相关代码上略逊于CodeWhisperer | 良好，随团队训练持续提升；企业版支持私有模型微调 | 优秀，尤其在AWS生态内（如Lambda、CloudFormation）准确率突出；通用场景略低于Copilot |\\n| **支持的语言范围** | 40+ 种语言（含Python、JS/TS、Java、Go、Ruby、C#等主流及小众语言） | 30+ 种语言（覆盖主流语言，对新兴语言支持较快） | 15+ 种语言（聚焦Python、Java、JavaScript、TypeScript、C#、Go等核心语言，AWS SDK深度优化） |\\n| **价格** | 个人版：$10/月 或 $100/年；商业版：$19/用户/月 | 免费基础版；Pro版：$12/用户/月；企业版：按需报价 | 个人版：免费；专业版：$19/用户/月；企业版：定制报价 |')]\n",
      "\n",
      "==================================================\n",
      "📍 当前节点: replanner\n",
      "  plan: ['基于对比结果，提出对于不同需求用户（如个人开发者、小型团队、大型企业）的选型建议。']\n",
      "\n",
      "==================================================\n",
      "📍 当前节点: executor\n",
      "  past_steps: [('基于对比结果，提出对于不同需求用户（如个人开发者、小型团队、大型企业）的选型建议。', '由于缺少具体的对比结果（如技术栈、工具、框架或平台的详细对比数据），无法基于实际对比得出针对个人开发者、小型团队和大型企业的选型建议。请提供相关对比结果，以便执行第 1 步。')]\n",
      "\n",
      "==================================================\n",
      "📍 当前节点: replanner\n",
      "  plan: ['根据收集到的信息，从代码补全准确率、支持的语言范围和价格三个维度对比通义灵码、文心快码和CodeGeeX这三个工具。', '基于更新后的对比结果，提出对于不同需求用户（如个人开发者、小型团队、大型企业）的选型建议。']\n",
      "\n",
      "==================================================\n",
      "📍 当前节点: executor\n",
      "  past_steps: [('根据收集到的信息，从代码补全准确率、支持的语言范围和价格三个维度对比通义灵码、文心快码和CodeGeeX这三个工具。', '| 维度             | 通义灵码                                     | 文心快码（Comate）                          | CodeGeeX                                  |\\n|------------------|----------------------------------------------|---------------------------------------------|-------------------------------------------|\\n| **代码补全准确率** | 高（9/10），上下文理解好，支持类型注解推断     | 中（7/10），算法正确但细节处理略逊于通义灵码 | 中高（7.5/10），算法正确但代码风格一致性一般 |\\n| **支持的语言范围** | Java、Python、Go、C#、C/C++、JavaScript、TypeScript、PHP、Ruby、Rust、Scala、Kotlin 等主流语言（300+语言提及） | 支持主流语言（具体列表未详列，但明确支持Java、Python、JS/TS等） | 支持300+语言，开源、学术背景强，覆盖广泛    |\\n| **价格**           | 59元/月（基础版免费）                         | 59元/月（标准版免费）                       | 完全免费，无限制                            |')]\n",
      "\n",
      "==================================================\n",
      "📍 当前节点: replanner\n",
      "  response: ### 2025年国内三大代码助手工具调研报告\n",
      "\n",
      "#### 1. 调研背景\n",
      "随着AI技术的发展，代码助手工具在提高开发效率、减少错误方面发挥了重要作用。本次调研针对2025年的通义灵码、文心快码（Comate）和CodeGeeX三款代码助手工具，从代码补全准确率、支持语言种类及价格三个维度进行对比分析，并基于此给出选型建议。\n",
      "\n",
      "#### 2. 工具概览\n",
      "\n",
      "- **通义灵码**：由阿里云推出，基于Q...\n"
     ]
    }
   ],
   "source": [
    "inputs = {\n",
    "    \"input\": (\n",
    "        \"帮我调研2025年国内三大代码助手工具（通义灵码、文心快码、CodeGeeX），\"\n",
    "        \"从代码补全准确率、支持语言、价格三个维度做对比，最后给出选型建议。\"\n",
    "        \"请用中文输出最终结果。\"\n",
    "    )\n",
    "}\n",
    "\n",
    "# stream 模式可以看到每一步的中间输出\n",
    "for event in app.stream(inputs):\n",
    "    for node_name, node_output in event.items():\n",
    "        print(f\"\\n{'='*50}\")\n",
    "        print(f\"📍 当前节点: {node_name}\")\n",
    "        # 避免打印太长的内容\n",
    "        for key, value in node_output.items():\n",
    "            if isinstance(value, str) and len(value) > 200:\n",
    "                print(f\"  {key}: {value[:200]}...\")\n",
    "            else:\n",
    "                print(f\"  {key}: {value}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "9f3414d9",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.messages import ToolMessage, AIMessage\n",
    "from langgraph.prebuilt import ToolNode, tools_condition\n",
    "from typing import Annotated\n",
    "from operator import add\n",
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "\n",
    "\n",
    "# ============================================================\n",
    "# 定义工具：模拟向外部系统发送消息\n",
    "# ============================================================\n",
    "@tool\n",
    "def send_message(channel: str, content: str) -> str:\n",
    "    \"\"\"向指定渠道发送消息。\n",
    "\n",
    "    Args:\n",
    "        channel: 发送渠道，\"wechat\"（企业微信）、\"email\"（邮件）、\"sms\"（短信）\n",
    "        content: 消息正文内容\n",
    "    \"\"\"\n",
    "    # 实际生产环境这里会调飞书/钉钉/企微的 API\n",
    "    return f\"📨 已通过 {channel} 发送：{content}\"\n",
    "\n",
    "\n",
    "tools = [send_message]\n",
    "\n",
    "# 模型绑定工具\n",
    "model_with_tools = ChatOpenAI(\n",
    "    model=os.getenv('QWEN_MODEL'),\n",
    "    api_key=os.getenv('QWEN_API_KEY'),\n",
    "    base_url=os.getenv('QWEN_API_BASE'),\n",
    ").bind_tools(tools)\n",
    "\n",
    "\n",
    "# ============================================================\n",
    "# 定义状态\n",
    "# ============================================================\n",
    "class MsgState(TypedDict):\n",
    "    messages: Annotated[list, add]   # add 表示追加而非覆盖\n",
    "\n",
    "\n",
    "# ============================================================\n",
    "# 定义节点\n",
    "# ============================================================\n",
    "def chatbot_node(state: MsgState) -> dict:\n",
    "    \"\"\"LLM 推理节点：决定是否调工具、调哪个工具\"\"\"\n",
    "    response = model_with_tools.invoke(state[\"messages\"])\n",
    "    return {\"messages\": [response]}\n",
    "\n",
    "\n",
    "# 工具执行节点（LangGraph 内置）\n",
    "tool_node = ToolNode(tools=tools)\n",
    "\n",
    "\n",
    "# ============================================================\n",
    "# 构建图：chatbot → 条件判断 → tools → chatbot（循环）\n",
    "# ============================================================\n",
    "builder = StateGraph(MsgState)\n",
    "builder.add_node(\"chatbot\", chatbot_node)\n",
    "builder.add_node(\"tools\", tool_node)\n",
    "\n",
    "builder.set_entry_point(\"chatbot\")\n",
    "builder.add_conditional_edges(\n",
    "    \"chatbot\",\n",
    "    tools_condition,           # 内置函数：有 tool_calls → \"tools\"，否则 → END\n",
    "    {\"tools\": \"tools\", \"__end__\": END},\n",
    ")\n",
    "builder.add_edge(\"tools\", \"chatbot\")\n",
    "\n",
    "# 编译 + 断点：在工具节点执行前暂停\n",
    "memory = MemorySaver()\n",
    "graph = builder.compile(\n",
    "    checkpointer=memory,\n",
    "    interrupt_before=[\"tools\"],  # 👈 工具实际调用前拦截\n",
    ")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3.11 (shuheAI)",
   "language": "python",
   "name": "shuheai"
  },
  "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.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
