{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "1d2da952",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'count': 4}\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 10892 characters]",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from langgraph.graph import StateGraph, START, END\n",
    "from typing import TypedDict\n",
    "\n",
    "# ============================================================\n",
    "# 第 1 步：定义 State —— 描述整个工作流在运行时的数据结构\n",
    "# ============================================================\n",
    "\n",
    "class CounterState(TypedDict):\n",
    "    \"\"\"工作流的状态：一个简单的计数器\"\"\"\n",
    "    count: int\n",
    "\n",
    "# ============================================================\n",
    "# 第 2 步：写节点函数 —— 每个节点做一件事\n",
    "# ============================================================\n",
    "\n",
    "def step_one(state: CounterState) -> dict:\n",
    "    \"\"\"第一个处理节点： count + 1\"\"\"\n",
    "    new_count = state[\"count\"] + 1\n",
    "    return {\n",
    "        \"count\": new_count\n",
    "    }\n",
    "\n",
    "def step_two(state: CounterState) -> dict:\n",
    "    \"\"\"第二个处理节点： count * 2\"\"\"\n",
    "    new_count = state[\"count\"] * 2\n",
    "    return {\n",
    "        \"count\": new_count\n",
    "    }\n",
    "\n",
    "# ============================================================\n",
    "# 第 3 步：创建 StateGraph 对象\n",
    "# ============================================================\n",
    "\n",
    "\n",
    "builder = StateGraph(CounterState)\n",
    "\n",
    "# ============================================================\n",
    "# 第 4 步：注册节点，连接边\n",
    "# ============================================================\n",
    "\n",
    "\n",
    "builder.add_node(\"step_one\", step_one)\n",
    "builder.add_node(\"step_two\", step_two)\n",
    "\n",
    "builder.add_edge(START, \"step_one\")\n",
    "builder.add_edge(\"step_one\", \"step_two\")\n",
    "builder.add_edge(\"step_two\", END)\n",
    "\n",
    "graph = builder.compile()\n",
    "\n",
    "result = graph.invoke({\"count\":1,\"log\":[]})\n",
    "print(result)\n",
    "\n",
    "# 显示图结构\n",
    "from IPython.display import Image,display\n",
    "display(Image(graph.get_graph().draw_mermaid_png()))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "2abea702",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "当前 count = 1,执行 + 1\n",
      "count未到达 2,继续循环\n",
      "当前 count = 2,执行 + 1\n",
      "count未到达 3,继续循环\n",
      "当前 count = 3,执行 + 1\n",
      "count未到达 4,继续循环\n",
      "当前 count = 4,执行 + 1\n",
      "count已到达 5,停止循环\n",
      "最终结果：{'count': 5}\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 11084 characters]",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from langgraph.graph import StateGraph, START ,END\n",
    "from typing import TypedDict\n",
    "\n",
    "class CountState(TypedDict):\n",
    "    count: int\n",
    "\n",
    "def increment(state: CountState) -> dict:\n",
    "    \"\"\"每次调用 count + 1\"\"\"\n",
    "    print(f\"当前 count = {state['count']},执行 + 1\")\n",
    "    return {\"count\":state[\"count\"]+1}\n",
    "\n",
    "def router(state: CountState) -> str:\n",
    "    \"\"\"\n",
    "    条件路由函数 —— LangGraph 的\"方向控制器\"\n",
    "\n",
    "    返回 END 意味着图执行结束\n",
    "    返回节点名意味着跳转到那个节点继续执行\n",
    "\n",
    "    注意：返回的必须是节点名（字符串），不是函数引用\n",
    "    \"\"\"\n",
    "    if state[\"count\"] >= 5:\n",
    "        print(f\"count已到达 {state['count']},停止循环\")\n",
    "        return \"quit\"\n",
    "    else:\n",
    "        print(f\"count未到达 {state['count']},继续循环\")\n",
    "        return \"incr\"  # 回到increment 节点\n",
    "\n",
    "# 构图\n",
    "builder = StateGraph(CountState)\n",
    "builder.add_node(\"increment\", increment)\n",
    "\n",
    "builder.add_edge(START, \"increment\")\n",
    "\n",
    "builder.add_conditional_edges(\n",
    "    \"increment\", # 从哪个节点出发\n",
    "    router, # 条件路由函数\n",
    "    {\"incr\": \"increment\", \"quit\": END} #返回值 -》目标节点的映射\n",
    ")\n",
    "\n",
    "graph = builder.compile()\n",
    "result = graph.invoke({\"count\":1})\n",
    "print(f\"最终结果：{result}\")\n",
    "\n",
    "from IPython.display import Image,display\n",
    "display(Image(graph.get_graph().draw_mermaid_png()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "cebdfb11",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['init', 'x1', 'x2', 'y1', 'z1', 'z2', 'z3']\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 9572 characters]",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from typing import Annotated, TypedDict\n",
    "from operator import add\n",
    "from langgraph.graph import StateGraph, START, END\n",
    "\n",
    "class AppendState(TypedDict):\n",
    "    \"\"\"Annotated[list, add] 告诉 Langgraph：\n",
    "    当多个节点都要更新items时， 用add（即（list + list2））合并\"\"\"\n",
    "    items: Annotated[list[str], add]\n",
    "\n",
    "def node_x(state: AppendState) -> dict:\n",
    "    return {\"items\": [\"x1\", \"x2\"]}\n",
    "\n",
    "def node_y(state: AppendState) -> dict:\n",
    "    return {\"items\": [\"y1\"]}\n",
    "\n",
    "def node_z(state: AppendState) -> dict:\n",
    "    return {\"items\": [\"z1\", \"z2\", \"z3\"]}\n",
    "\n",
    "builder = StateGraph(AppendState)\n",
    "builder.add_node(\"x\", node_x)\n",
    "builder.add_node(\"y\", node_y)\n",
    "builder.add_node(\"z\", node_z)\n",
    "\n",
    "builder.add_edge(START, \"x\")\n",
    "builder.add_edge(\"x\", \"y\")\n",
    "builder.add_edge(\"y\", \"z\")\n",
    "builder.add_edge(\"z\", END)\n",
    "\n",
    "graph = builder.compile()\n",
    "result = graph.invoke({\"items\": [\"init\"]})\n",
    "print(result[\"items\"])\n",
    "\n",
    "from IPython.display import Image,display\n",
    "display(Image(graph.get_graph().draw_mermaid_png()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "2938b4e9",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[HumanMessage(content='你好,我是王琪', additional_kwargs={}, response_metadata={}, id='f6da4a6b-e9c4-4fe6-a316-4f7e3c3697cb'), AIMessage(content='你好，王琪！很高兴认识你。有什么我可以帮你的吗？无论是学习、工作、生活中的问题，还是随便聊聊，我都在这里随时为你服务。😊', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 372, 'prompt_tokens': 15, 'total_tokens': 387, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 333, 'rejected_prediction_tokens': None, 'text_tokens': 372}, 'prompt_tokens_details': {'audio_tokens': None, 'cache_write_tokens': None, 'cached_tokens': None, 'text_tokens': 15}}, 'model_provider': 'openai', 'model_name': 'qwen3.6-plus', 'system_fingerprint': None, 'id': 'chatcmpl-fc21ad6e-3186-9c08-8aa2-66e12961da60', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019f9209-e1d7-7ea2-b47b-e72a291c8afc-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 15, 'output_tokens': 372, 'total_tokens': 387, 'input_token_details': {}, 'output_token_details': {'reasoning': 333}})]\n"
     ]
    }
   ],
   "source": [
    "from langgraph.graph import MessagesState\n",
    "from langgraph.graph import StateGraph, START, END\n",
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.messages import HumanMessage\n",
    "from dotenv import load_dotenv\n",
    "import os\n",
    "\n",
    "load_dotenv()\n",
    "api_key = os.getenv('QWEN_API_KEY')\n",
    "\n",
    "model = ChatOpenAI(\n",
    "    model=os.getenv('QWEN_MODEL'),\n",
    "    api_key=api_key,\n",
    "    base_url=os.getenv('QWEN_API_BASE'),\n",
    ")\n",
    "\n",
    "def chat_node(state: MessagesState) -> dict:\n",
    "    \"\"\"调用LLM 返回的消息会自动追加到历史里\"\"\"\n",
    "    response = model.invoke(state[\"messages\"])\n",
    "    return {\"messages\": [response]}\n",
    "\n",
    "builder = StateGraph(MessagesState)\n",
    "builder.add_node(\"chat\", chat_node)\n",
    "builder.add_edge(START, \"chat\")\n",
    "builder.add_edge(\"chat\", END)\n",
    "\n",
    "graph = builder.compile()\n",
    "\n",
    "# 执行： 传入一条 HumanMessage\n",
    "result = graph.invoke({\"messages\": [HumanMessage(content=\"你好,我是王琪\")]})\n",
    "print(result['messages'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "72689032",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['reset_data', 'cherry']\n"
     ]
    }
   ],
   "source": [
    "from typing import Annotated, TypedDict\n",
    "from langgraph.graph import StateGraph, START, END\n",
    "\n",
    "class ReplaceableList(list):\n",
    "    \"\"\"\n",
    "    自定义列表子类，带一个 replace 标记。\n",
    "\n",
    "    用法：\n",
    "    - 普通 list → 追加到已有数据\n",
    "    - ReplaceableList([...], replace=True) → 全量覆盖已有数据\n",
    "    \"\"\"\n",
    "    def __init__(self, *args, replace: bool = False, **kwargs):\n",
    "        super().__init__(*args, **kwargs)\n",
    "        self.replace = replace\n",
    "\n",
    "def smart_merge(current: list, incoming: list) -> list:\n",
    "    \"\"\"\n",
    "    智能合并函数 —— 作为自定义 Reducer 传给 Annotated。\n",
    "\n",
    "    签名必须满足 (旧值, 新值) → 合并后的值。\n",
    "    LangGraph 会在节点返回时自动调用此函数。\n",
    "    \"\"\"\n",
    "    # 检查传入的是否是 ReplaceableList 且标记了 replace=True\n",
    "    if isinstance(incoming, ReplaceableList) and getattr(incoming, \"replace\", False):\n",
    "        return list(incoming)         # 覆盖模式：直接用新数据替换\n",
    "    \n",
    "    return current + list(incoming)   # 追加模式：新数据拼到旧数据后面\n",
    "class SmartState(TypedDict):\n",
    "    items: Annotated[list[str], smart_merge]  # ← 使用自定义 Reducer\n",
    "\n",
    "\n",
    "def add_some(state: SmartState) -> dict:\n",
    "    \"\"\"普通列表 → 追加\"\"\"\n",
    "    return {\"items\": [\"apple\", \"banana\"]}\n",
    "\n",
    "def replace_all(state: SmartState) -> dict:\n",
    "    \"\"\"带 replace 标记的列表 → 全覆盖\"\"\"\n",
    "    new_list = ReplaceableList([\"reset_data\"])\n",
    "    new_list.replace = True\n",
    "    return {\"items\": new_list}\n",
    "\n",
    "def add_more(state: SmartState) -> dict:\n",
    "    \"\"\"再追加一条\"\"\"\n",
    "    return {\"items\": [\"cherry\"]}\n",
    "\n",
    "\n",
    "builder = StateGraph(SmartState)\n",
    "builder.add_node(\"add_some\", add_some)\n",
    "builder.add_node(\"replace_all\", replace_all)\n",
    "builder.add_node(\"add_more\", add_more)\n",
    "\n",
    "builder.add_edge(START, \"add_some\")\n",
    "builder.add_edge(\"add_some\", \"replace_all\")\n",
    "builder.add_edge(\"replace_all\", \"add_more\")\n",
    "builder.add_edge(\"add_more\", END)\n",
    "\n",
    "graph = builder.compile()\n",
    "result = graph.invoke({\"items\": [\"init\"]})\n",
    "print(result[\"items\"])\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "2f8eea8f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "开始： 拿出杯子\n",
      "1. 研磨咖啡豆\n",
      "2. 冲泡咖啡\n",
      "3. 加牛奶\n",
      "结束： 品尝咖啡\n"
     ]
    }
   ],
   "source": [
    "from typing import Annotated, TypedDict\n",
    "from operator import add\n",
    "from langgraph.graph import StateGraph, START, END\n",
    "\n",
    "class CoffeeState(TypedDict):\n",
    "    logs: Annotated[list[str], add]  # 记录每一步操作\n",
    "# 子图使用独立的状态 schema，避免共享主图的 logs\n",
    "# 子图只关心自己内部产生的日志\n",
    "\n",
    "class SubCoffeeState(TypedDict):\n",
    "    sub_logs: Annotated[list[str], add]\n",
    "\n",
    "# 子图：泡咖啡（内部3个步骤）\n",
    "\n",
    "def grind_beans(state: SubCoffeeState) -> dict:\n",
    "    return {\"sub_logs\": [\"1. 研磨咖啡豆\"]}\n",
    "\n",
    "def brew_coffee(state: SubCoffeeState) -> dict:\n",
    "    return {\"sub_logs\": [\"2. 冲泡咖啡\"]}\n",
    "\n",
    "def add_milk(state: SubCoffeeState) -> dict:\n",
    "    return {\"sub_logs\": [\"3. 加牛奶\"]}\n",
    "\n",
    "# 构建子图 - 使用独立的SubCoffeeState\n",
    "coffee_builder = StateGraph(SubCoffeeState)\n",
    "coffee_builder.add_node(\"grind\", grind_beans)\n",
    "coffee_builder.add_node(\"brew\", brew_coffee)\n",
    "coffee_builder.add_node(\"add_milk\", add_milk)\n",
    "\n",
    "coffee_builder.add_edge(START, \"grind\")\n",
    "coffee_builder.add_edge(\"grind\", \"brew\")\n",
    "coffee_builder.add_edge(\"brew\", \"add_milk\")\n",
    "coffee_builder.add_edge(\"add_milk\", END)\n",
    "\n",
    "coffee_subgraph = coffee_builder.compile()\n",
    "\n",
    "# 主图：完整的泡咖啡的过程\n",
    "\n",
    "def prepare_cup(state: CoffeeState) -> dict:\n",
    "    return {\"logs\": [\"开始： 拿出杯子\"]}\n",
    "\n",
    "def make_coffee(state: CoffeeState) -> dict:\n",
    "    # 子图使用自己的状态，初始 sub_logs 为空\n",
    "    sub_result = coffee_subgraph.invoke({\"sub_logs\": []})\n",
    "    # 把子图的 sub_logs 映射回主图的 logs\n",
    "    return {\"logs\": sub_result[\"sub_logs\"]}\n",
    "\n",
    "def enjoy(state: CoffeeState) -> dict:\n",
    "    return {\"logs\": [\"结束： 品尝咖啡\"]}\n",
    "\n",
    "# 构建主图\n",
    "main_builder = StateGraph(CoffeeState)\n",
    "main_builder.add_node(\"prepare\", prepare_cup)\n",
    "main_builder.add_node(\"make\", make_coffee)  # 使用包装函数（内部做状态映射）\n",
    "main_builder.add_node(\"enjoy\", enjoy)\n",
    "\n",
    "main_builder.add_edge(START, \"prepare\")\n",
    "main_builder.add_edge(\"prepare\", \"make\")\n",
    "main_builder.add_edge(\"make\", \"enjoy\")\n",
    "main_builder.add_edge(\"enjoy\", END)\n",
    "\n",
    "main_graph = main_builder.compile()\n",
    "\n",
    "# 运行\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    result = main_graph.invoke({\"logs\": []})\n",
    "\n",
    "    for step in result[\"logs\"]:\n",
    "        print(step)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "2d9108d5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[parse] 收到问题: 帮我对比一下苹果、华为和小米的旗舰机型\n",
      "[parse] 解析出 3 个品牌: ['苹果', '华为', '小米']\n",
      "  [research] 并行查询 [苹果] -> iPhone 16 Pro Max | A18 Pro 芯片 | 6.9吋 OLED | 4K 120fps 视频 | iOS 18\n",
      "  [research] 并行查询 [华为] -> Mate 70 Pro+ | 麒麟 9100 | 6.8吋 OLED | 物理可变光圈 | 鸿蒙 NEXT\n",
      "  [research] 并行查询 [小米] -> 小米 15 Ultra | 骁龙 8 Gen4 | 6.73吋 AMOLED | 徕卡光学 | 澎湃 OS 2.0\n",
      "\n",
      "[summarize] 开始汇总 3 份报告...\n",
      "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n",
      "         >> 旗舰手机对比总结\n",
      "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n",
      "\n",
      "  【苹果】iPhone 16 Pro Max | A18 Pro 芯片 | 6.9吋 OLED | 4K 120fps 视频 | iOS 18\n",
      "  【华为】Mate 70 Pro+ | 麒麟 9100 | 6.8吋 OLED | 物理可变光圈 | 鸿蒙 NEXT\n",
      "  【小米】小米 15 Ultra | 骁龙 8 Gen4 | 6.73吋 AMOLED | 徕卡光学 | 澎湃 OS 2.0\n",
      "\n",
      "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n",
      "[总结] 三款旗舰各有千秋 --\n",
      "  苹果 iPhone 16 Pro Max：视频拍摄王者，生态闭环体验最佳\n",
      "  华为 Mate 70 Pro+：影像系统物理可变光圈独树一帜\n",
      "  小米 15 Ultra：徕卡光学加持，性价比旗舰首选\n",
      "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n",
      "\n"
     ]
    }
   ],
   "source": [
    "\"\"\"\n",
    "图的拓扑结构：\n",
    "\n",
    "            START\n",
    "              │\n",
    "              ▼\n",
    "        [parse_query]            ← 节点：解析用户输入，提取品牌列表\n",
    "              │\n",
    "              ▼\n",
    "        route_to_research        ← 条件边路由函数：返回 N 个 Send\n",
    "              │\n",
    "              ├─ Send(\"research_brand\", {brand: \"苹果\"})\n",
    "              ├─ Send(\"research_brand\", {brand: \"华为\"})    ← 三个并行\n",
    "              └─ Send(\"research_brand\", {brand: \"小米\"})\n",
    "              │\n",
    "              ▼  (并行执行)\n",
    "        [research_brand] × 3     ← 每个品牌独立研究\n",
    "              │\n",
    "              ▼  (全部完成后汇总)\n",
    "        [summarize]              ← 节点：汇总所有报告，输出对比\n",
    "              │\n",
    "              ▼\n",
    "             END\n",
    "\"\"\"\n",
    "from typing import Annotated, TypedDict\n",
    "from operator import add\n",
    "from langgraph.graph import StateGraph, START, END\n",
    "from langgraph.types import Send\n",
    "\n",
    "\n",
    "# ══════════════════════════════════════════════════════════════\n",
    "#  State 定义\n",
    "# ══════════════════════════════════════════════════════════════\n",
    "class CompareState(TypedDict):\n",
    "    \"\"\"手机对比任务的 State\"\"\"\n",
    "    query: str                                # 用户的原始问题\n",
    "    brands: list[str]                         # 从 query 中解析出的品牌列表\n",
    "    reports: Annotated[list[str], add]        # 每个品牌的研究报告（add reducer 自动合并）\n",
    "    comparison: str                           # 最终的对比总结\n",
    "\n",
    "\n",
    "# ══════════════════════════════════════════════════════════════\n",
    "#  节点函数\n",
    "# ══════════════════════════════════════════════════════════════\n",
    "def parse_query(state: CompareState) -> dict:\n",
    "    \"\"\"\n",
    "    节点 ①：解析用户输入，提取出要对比的品牌列表。\n",
    "\n",
    "    实际场景中这里会用 LLM 做实体提取，这里直接硬编码模拟。\n",
    "    \"\"\"\n",
    "    query = state[\"query\"]\n",
    "    print(f\"[parse] 收到问题: {query}\")\n",
    "\n",
    "    # 模拟 LLM 解析：从自然语言中提取品牌名称\n",
    "    brands = [\"苹果\", \"华为\", \"小米\"]\n",
    "    print(f\"[parse] 解析出 {len(brands)} 个品牌: {brands}\")\n",
    "\n",
    "    return {\"brands\": brands}\n",
    "\n",
    "\n",
    "def research_brand(state: CompareState) -> dict:\n",
    "    \"\"\"\n",
    "    节点 ②：研究单个品牌（被 Send 并行调用的目标节点）。\n",
    "\n",
    "    它不知道总共有几个品牌，也不关心其他品牌在做什么——\n",
    "    只专注于自己拿到的这一个。\n",
    "\n",
    "    实际场景中这里会调搜索引擎 API 或 LLM，这里用数据模拟。\n",
    "    \"\"\"\n",
    "    brand = state[\"brands\"][0]  # Send 保证这里只有一个品牌\n",
    "\n",
    "    # 模拟：查询该品牌的旗舰机信息\n",
    "    phone_db = {\n",
    "        \"苹果\": \"iPhone 16 Pro Max | A18 Pro 芯片 | 6.9吋 OLED | 4K 120fps 视频 | iOS 18\",\n",
    "        \"华为\": \"Mate 70 Pro+ | 麒麟 9100 | 6.8吋 OLED | 物理可变光圈 | 鸿蒙 NEXT\",\n",
    "        \"小米\": \"小米 15 Ultra | 骁龙 8 Gen4 | 6.73吋 AMOLED | 徕卡光学 | 澎湃 OS 2.0\",\n",
    "    }\n",
    "\n",
    "    info = phone_db.get(brand, f\"{brand}旗舰机信息暂缺\")\n",
    "    report = f\"【{brand}】{info}\"\n",
    "\n",
    "    print(f\"  [research] 并行查询 [{brand}] -> {info}\")\n",
    "    return {\"reports\": [report]}  # add reducer 自动拼到总 reports 里\n",
    "\n",
    "\n",
    "def summarize(state: CompareState) -> dict:\n",
    "    \"\"\"\n",
    "    节点 ③：所有品牌都研究完了，汇总生成对比结论。\n",
    "\n",
    "    实际场景中这里会把所有 reports 喂给 LLM，让它生成对比分析。\n",
    "    \"\"\"\n",
    "    print(f\"\\n[summarize] 开始汇总 {len(state['reports'])} 份报告...\")\n",
    "\n",
    "    # 模拟 LLM 生成的对比总结\n",
    "    comparison = (\n",
    "        \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n\"\n",
    "        \"         >> 旗舰手机对比总结\\n\"\n",
    "        \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n\\n\"\n",
    "    )\n",
    "    for report in state[\"reports\"]:\n",
    "        comparison += f\"  {report}\\n\"\n",
    "\n",
    "    comparison += (\n",
    "        \"\\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n\"\n",
    "        \"[总结] 三款旗舰各有千秋 --\\n\"\n",
    "        \"  苹果 iPhone 16 Pro Max：视频拍摄王者，生态闭环体验最佳\\n\"\n",
    "        \"  华为 Mate 70 Pro+：影像系统物理可变光圈独树一帜\\n\"\n",
    "        \"  小米 15 Ultra：徕卡光学加持，性价比旗舰首选\\n\"\n",
    "        \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n\"\n",
    "    )\n",
    "\n",
    "    return {\"comparison\": comparison}\n",
    "\n",
    "\n",
    "# ══════════════════════════════════════════════════════════════\n",
    "#  路由函数 —— 核心！返回 Send 列表而不是 dict\n",
    "# ══════════════════════════════════════════════════════════════\n",
    "def route_to_research(state: CompareState):\n",
    "    \"\"\"\n",
    "    ★ Send API 的核心：条件边路由函数 ★\n",
    "\n",
    "    关键规则：\n",
    "    - 这个函数只能放在 add_conditional_edges 里，不能放进 add_node\n",
    "    - 普通节点返回 dict（更新 state）\n",
    "    - 路由函数返回 Send 列表（创建并行任务）或字符串（END / 节点名）\n",
    "\n",
    "    思想：几个品牌 → 发几个 Send → 启动几次 research_brand\n",
    "\n",
    "            return [\n",
    "                Send(\"research_brand\", {\"brands\": [\"苹果\"]}),   ─┐\n",
    "                Send(\"research_brand\", {\"brands\": [\"华为\"]}),   ─┤ 全部并行执行\n",
    "                Send(\"research_brand\", {\"brands\": [\"小米\"]}),   ─┘\n",
    "            ]\n",
    "\n",
    "    每个 Send 的两个参数：\n",
    "      参数 1: 目标节点名 —— \"派给谁做\"\n",
    "      参数 2: 此分支专属的 state —— \"这个任务要什么数据\"\n",
    "    \"\"\"\n",
    "    if not state[\"brands\"]:\n",
    "        return END\n",
    "\n",
    "    return [\n",
    "        Send(\"research_brand\", {\"brands\": [brand]})\n",
    "        for brand in state[\"brands\"]\n",
    "    ]\n",
    "\n",
    "\n",
    "# ══════════════════════════════════════════════════════════════\n",
    "#  构图\n",
    "# ══════════════════════════════════════════════════════════════\n",
    "builder = StateGraph(CompareState)\n",
    "\n",
    "builder.add_node(\"parse_query\", parse_query)\n",
    "builder.add_node(\"research_brand\", research_brand)\n",
    "builder.add_node(\"summarize\", summarize)\n",
    "\n",
    "# 边\n",
    "builder.add_edge(START, \"parse_query\")\n",
    "builder.add_conditional_edges(\"parse_query\", route_to_research)  # 路由 → 动态 fan-out\n",
    "builder.add_edge(\"research_brand\", \"summarize\")                  # 所有并行结果汇聚到 summarize\n",
    "builder.add_edge(\"summarize\", END)\n",
    "\n",
    "graph = builder.compile()\n",
    "\n",
    "\n",
    "# ══════════════════════════════════════════════════════════════\n",
    "#  运行\n",
    "# ══════════════════════════════════════════════════════════════\n",
    "if __name__ == \"__main__\":\n",
    "    result = graph.invoke({\n",
    "        \"query\": \"帮我对比一下苹果、华为和小米的旗舰机型\",\n",
    "        \"brands\": [],\n",
    "        \"reports\": [],\n",
    "        \"comparison\": \"\",\n",
    "    })\n",
    "\n",
    "    print(result[\"comparison\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "e087f983",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "第一轮: 你好，老王！很高兴认识你。👋\n",
      "\n",
      "作为 Python 程序员，你平时主要在做哪个方向呢？比如 Web 后端（Django/FastAPI/Flask）、数据分析/科学计算、自动化运维/爬虫、AI/机器学习，还是其他领域？\n",
      "\n",
      "如果你遇到以下任何情况，随时丢过来：\n",
      "- 🐍 代码报错、性能瓶颈或调试难题\n",
      "- 📦 依赖管理、环境配置、打包部署（venv/poetry/pyproject.toml）\n",
      "- 🏗️ 项目结构、架构设计、测试与 CI/CD\n",
      "- 🔄 Python 3.12/3.13 新特性、类型提示、异步编程最佳实践\n",
      "- 💡 技术选型、学习路线或职业发展交流\n",
      "\n",
      "最近在用 Python 做什么项目？或者有什么特别想探讨/卡壳的技术点吗？我随时在线帮你梳理思路。\n",
      "第二轮: 你叫**老王**，是一名 **Python 程序员**。 \n",
      "\n",
      "如果有代码调试、架构设计、性能优化或者任何技术上的问题，随时丢过来，我帮你一起看！🐍💻\n"
     ]
    }
   ],
   "source": [
    "from langgraph.checkpoint.memory import InMemorySaver\n",
    "from langgraph.graph import StateGraph, START, END,MessagesState\n",
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.messages import HumanMessage\n",
    "\n",
    "# 初始化模型\n",
    "from dotenv import load_dotenv\n",
    "import os\n",
    "\n",
    "load_dotenv()\n",
    "api_key = os.getenv('QWEN_API_KEY')\n",
    "\n",
    "model = ChatOpenAI(\n",
    "    model=os.getenv('QWEN_MODEL'),\n",
    "    api_key=api_key,\n",
    "    base_url=os.getenv('QWEN_API_BASE'),\n",
    ")\n",
    "\n",
    "\n",
    "\n",
    "def chat(state: MessagesState) -> dict:\n",
    "    \"\"\"调用 LLM 生成回复\"\"\"\n",
    "    response = model.invoke(state[\"messages\"])\n",
    "    return {\"messages\": [response]}\n",
    "\n",
    "\n",
    "# 构建最简聊天图\n",
    "builder = StateGraph(MessagesState)\n",
    "builder.add_node(\"chat\", chat)\n",
    "builder.add_edge(START, \"chat\")\n",
    "builder.add_edge(\"chat\", END)\n",
    "\n",
    "# ★ 关键：compile 时传入 checkpointer\n",
    "memory = InMemorySaver()\n",
    "graph = builder.compile(checkpointer=memory)\n",
    "\n",
    "# ============================================================\n",
    "# 第一次对话\n",
    "# ============================================================\n",
    "config = {\"configurable\": {\"thread_id\": \"session-001\"}}\n",
    "result = graph.invoke(\n",
    "    {\"messages\": [HumanMessage(content=\"你好，我叫老王，是一名 Python 程序员\")]},\n",
    "    config=config,   # ← 必须传入 config 来标识会话\n",
    ")\n",
    "print(\"第一轮:\", result[\"messages\"][-1].content)\n",
    "\n",
    "# ============================================================\n",
    "# 第二次对话（同一个 thread_id）\n",
    "# ============================================================\n",
    "result = graph.invoke(\n",
    "    {\"messages\": [HumanMessage(content=\"我叫什么名字？做什么工作？\")]},\n",
    "    config=config,   # ← 同一个 thread_id，自动加载之前的记忆\n",
    ")\n",
    "print(\"第二轮:\", result[\"messages\"][-1].content)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "d8e08b2d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "session-001: 你叫**老王**。  \n",
      "\n",
      "如果有代码问题、技术探讨或者需要帮忙梳理思路，随时告诉我！🐍💻\n",
      "session-002: 我不知道您的名字。作为一个人工智能，我无法获取您的个人信息。如果您愿意告诉我，我很乐意记住并这样称呼您！\n"
     ]
    }
   ],
   "source": [
    "# 用老王的名义问\n",
    "result_a = graph.invoke(\n",
    "    {\"messages\": [HumanMessage(content=\"我叫什么？\")]},\n",
    "    config={\"configurable\": {\"thread_id\": \"session-001\"}},  # 老王的 thread\n",
    ")\n",
    "print(\"session-001:\", result_a[\"messages\"][-1].content)  # \"你叫老王\"（有记忆）\n",
    "\n",
    "# 换一个 thread_id\n",
    "result_b = graph.invoke(\n",
    "    {\"messages\": [HumanMessage(content=\"我叫什么？\")]},\n",
    "    config={\"configurable\": {\"thread_id\": \"session-002\"}},  # 全新的 thread\n",
    ")\n",
    "print(\"session-002:\", result_b[\"messages\"][-1].content)  # \"我不知道\"（没记忆）"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "57802de0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "快照 1: 6 条消息, 下一个节点: ()\n",
      "快照 2: 5 条消息, 下一个节点: ('chat',)\n",
      "快照 3: 4 条消息, 下一个节点: ('__start__',)\n",
      "快照 4: 4 条消息, 下一个节点: ()\n",
      "快照 5: 3 条消息, 下一个节点: ('chat',)\n",
      "快照 6: 2 条消息, 下一个节点: ('__start__',)\n",
      "快照 7: 2 条消息, 下一个节点: ()\n",
      "快照 8: 1 条消息, 下一个节点: ('chat',)\n",
      "快照 9: 0 条消息, 下一个节点: ('__start__',)\n"
     ]
    }
   ],
   "source": [
    "# 查看 session-001 的所有历史快照\n",
    "history = list(graph.get_state_history(\n",
    "    config={\"configurable\": {\"thread_id\": \"session-001\"}}\n",
    "))\n",
    "\n",
    "# history 按时间倒序排列，[0] 是最新状态，[-1] 是最早的状态\n",
    "for i, snapshot in enumerate(history):\n",
    "    msg_count = len(snapshot.values.get(\"messages\", []))\n",
    "    next_node = snapshot.next   # 快照之后接下来要执行的节点（如果是空元组表示已结束）\n",
    "    print(f\"快照 {i + 1}: {msg_count} 条消息, 下一个节点: {next_node}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "9df0f7e5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "好的，我记住了！你最喜欢的语言是 **Python**。在接下来的对话中，我会优先围绕 Python 为你提供帮助，无论是写代码、调试、优化、学习资源还是项目思路，随时告诉我你的需求！🐍💻\n"
     ]
    }
   ],
   "source": [
    "from langgraph.checkpoint.redis import RedisSaver\n",
    "from langgraph.graph import StateGraph, START, END, MessagesState\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "# 初始化模型\n",
    "from dotenv import load_dotenv\n",
    "import os\n",
    "\n",
    "load_dotenv()\n",
    "api_key = os.getenv('QWEN_API_KEY')\n",
    "\n",
    "model = ChatOpenAI(\n",
    "    model=os.getenv('QWEN_MODEL'),\n",
    "    api_key=api_key,\n",
    "    base_url=os.getenv('QWEN_API_BASE'),\n",
    ")\n",
    "\n",
    "\n",
    "def chat(state: MessagesState) -> dict:\n",
    "    response = model.invoke(state[\"messages\"])\n",
    "    return {\"messages\": [response]}\n",
    "\n",
    "\n",
    "# 使用 Redis 持久化\n",
    "REDIS_URL = \"redis://localhost:6379/0\"\n",
    "\n",
    "with RedisSaver.from_conn_string(REDIS_URL) as checkpointer:\n",
    "    # checkpointer.setup()  # 首次使用需要执行一次初始化\n",
    "\n",
    "    builder = StateGraph(MessagesState)\n",
    "    builder.add_node(\"chat\", chat)\n",
    "    builder.add_edge(START, \"chat\")\n",
    "    builder.add_edge(\"chat\", END)\n",
    "\n",
    "    graph = builder.compile(checkpointer=checkpointer)\n",
    "\n",
    "    config = {\"configurable\": {\"thread_id\": \"user-42\"}}\n",
    "    result = graph.invoke(\n",
    "        {\"messages\": [{\"role\": \"user\", \"content\": \"记住：我最喜欢的语言是 Python\"}]},\n",
    "        config=config,\n",
    "    )\n",
    "    print(result[\"messages\"][-1].content)\n",
    "    # 即使进程重启，只要 Redis 还在，记忆就不会丢"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "1e90d945",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[b'checkpoint_write:user-42:__empty__:1f1871a4-f9d8-6312-8000-7d460c287542:15514895-f7b2-d38c-1196-ec3089bc6a05:0', b'checkpoint:user-42:__empty__:1f1871a4-f9ce-66e6-bfff-ad92fc4e7fd2', b'checkpoint_latest:user-42:__empty__', b'checkpoint:user-42:__empty__:1f1871a4-f9d8-6312-8000-7d460c287542', b'checkpoint_write:user-42:__empty__:1f1871a4-f9ce-66e6-bfff-ad92fc4e7fd2:191c2321-6d17-59e7-e3e2-18379de588ae:0', b'checkpoint_write:user-42:__empty__:1f1871a4-f9ce-66e6-bfff-ad92fc4e7fd2:191c2321-6d17-59e7-e3e2-18379de588ae:1', b'checkpoint:user-42:__empty__:1f1871a5-6985-66eb-8001-baf6516ff08e']\n"
     ]
    }
   ],
   "source": [
    "import redis\n",
    "# 连接到本地Redis实例\n",
    "r = redis.Redis(host='localhost', port=6379, db=0)\n",
    "# 查看所有键\n",
    "keys = r.keys('*')\n",
    "print(keys)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "38434611",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "  [6f56dee0-a814-4404-a344-9f99ae8b4e41] 用户叫老王，是 Python 程序员\n",
      "  [cd26dc88-61c0-4dbe-a7f7-e4f790fa7688] 用户偏好简洁的代码风格\n",
      "  [61b8bdc3-4b72-4c05-9e05-2403da10f75c] 用户最近在学 LangGraph\n",
      "[('memories', 'user_42')]\n"
     ]
    }
   ],
   "source": [
    "from langgraph.store.memory import InMemoryStore\n",
    "import uuid\n",
    "\n",
    "# ============================================================\n",
    "# Store 的数据模型：\n",
    "# store\n",
    "# └── namespace: (\"memories\", \"user_42\")    ← 命名空间，按用户/业务隔离\n",
    "#     ├── key: \"uuid-1\"                     ← 唯一键，通常用 UUID\n",
    "#     │   └── value: {\"data\": \"...\"}        ← 实际存储的值\n",
    "#     └── key: \"uuid-2\"\n",
    "#         └── value: {\"data\": \"...\"}\n",
    "# ============================================================\n",
    "\n",
    "store = InMemoryStore()\n",
    "\n",
    "# 写入记忆\n",
    "ns = (\"memories\", \"user_42\")\n",
    "store.put(ns, str(uuid.uuid4()), {\"data\": \"用户叫老王，是 Python 程序员\"})\n",
    "store.put(ns, str(uuid.uuid4()), {\"data\": \"用户偏好简洁的代码风格\"})\n",
    "store.put(ns, str(uuid.uuid4()), {\"data\": \"用户最近在学 LangGraph\"})\n",
    "\n",
    "# 语义搜索记忆（Store 内置了向量搜索能力，不需要额外的向量数据库）\n",
    "results = store.search(ns, query=\"用户的名字和工作\")\n",
    "for item in results:\n",
    "    print(f\"  [{item.key}] {item.value['data']}\")\n",
    "# 输出：\n",
    "#   [uuid-1] 用户叫老王，是 Python 程序员\n",
    "#   [uuid-3] 用户最近在学 LangGraph\n",
    "\n",
    "# 精确获取（知道 key 时使用）\n",
    "# item = store.get(ns, key=\"某个uuid\")\n",
    "# print(item.value[\"data\"])\n",
    "\n",
    "# 列出所有命名空间\n",
    "all_ns = store.list_namespaces(prefix=(\"memories\",))\n",
    "print(list(all_ns))  # [('memories', 'user_42'), ...]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "55e1c47e",
   "metadata": {},
   "outputs": [],
   "source": [
    "from dataclasses import dataclass\n",
    "\n",
    "\n",
    "@dataclass\n",
    "class UserContext:\n",
    "    \"\"\"运行时上下文：描述当前请求的\"环境信息\"。\n",
    "\n",
    "    这些信息不适合放在 State 里，因为：\n",
    "    - user_id 是调用者身份，不是对话内容\n",
    "    - tier 是权限级别，不是业务状态\n",
    "    - 每次调用可能不同，但不应影响 State 的结构\n",
    "    \"\"\"\n",
    "    user_id: str          # 谁在调用\n",
    "    tier: str = \"free\"    # 用户等级：free / premium\n",
    "\n",
    "# 在 invoke 时传入：\n",
    "# graph.invoke(\n",
    "#     {\"messages\": [...]},\n",
    "#     config={\"configurable\": {\"thread_id\": \"1\"}},\n",
    "#     context=UserContext(user_id=\"alice\", tier=\"premium\"),\n",
    "# )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "7b21b743",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "会话1: user_42\n",
      "💾 已存入: 我喜欢 Python 和 LangGraph\n",
      "好的，我已经记下了你的偏好：**喜欢 Python 和 LangGraph**。\n",
      "\n",
      "在后续的对话中，我会优先结合这两项技术为你提供代码示例、架构建议或学习资源。如果你有具体的项目构思、调试问题，或想了解如何将 LangGraph 应用到实际工作流中，随时告诉我！\n",
      "\n",
      "会话2: user_42（换 thread）\n",
      "根据我之前记录的信息，您提到过自己喜欢 **Python** 和 **LangGraph**。如果还有其他偏好需要更新或补充，随时告诉我！\n",
      "\n",
      "会话3: user_99（不同用户）\n",
      "作为您的私人助理，我目前还没有收到过您关于个人喜好的信息。由于我们的对话是全新开始的，我暂时还不知道您喜欢什么。\n",
      "\n",
      "如果您愿意，现在可以告诉我您的兴趣爱好、饮食偏好、生活习惯或任何您希望我记住的内容。我会帮您妥善记录，以便在之后的交流中为您提供更贴心、个性化的服务！\n"
     ]
    }
   ],
   "source": [
    "from dataclasses import dataclass\n",
    "from langgraph.graph import StateGraph, MessagesState, START, END\n",
    "from langgraph.store.memory import InMemoryStore\n",
    "from langgraph.runtime import Runtime\n",
    "import uuid\n",
    "\n",
    "@dataclass\n",
    "class Context:\n",
    "    user_id: str\n",
    "\n",
    "# ── 节点：LLM + Store 读写 ──\n",
    "def chat_node(state: MessagesState, runtime: Runtime[Context]):\n",
    "    user_id = runtime.context.user_id\n",
    "    ns = (\"memories\", user_id)\n",
    "    last_msg = state[\"messages\"][-1].content\n",
    "\n",
    "    # 搜索历史记忆，拼入 system prompt\n",
    "    memories = runtime.store.search(ns, query=last_msg)\n",
    "    history = \"\\n\".join([f\"- {m.value['data']}\" for m in memories]) or \"暂无\"\n",
    "    system = f\"你是用户的私人助理。以下是对该用户的已知信息:\\n{history}\"\n",
    "\n",
    "    # 调用 LLM\n",
    "    result = model.invoke([{\"role\": \"system\", \"content\": system}] + state[\"messages\"])\n",
    "\n",
    "    # 如果用户让\"记住\"，写入 Store\n",
    "    if \"记住\" in last_msg:\n",
    "        info = last_msg.split(\"记住\", 1)[-1].strip()\n",
    "        runtime.store.put(ns, str(uuid.uuid4()), {\"data\": info})\n",
    "        print(f\"💾 已存入: {info}\")\n",
    "\n",
    "    return {\"messages\": [result]}\n",
    "\n",
    "# ── 构图 ──\n",
    "store = InMemoryStore()\n",
    "builder = StateGraph(MessagesState, context_schema=Context)\n",
    "builder.add_node(\"chat\", chat_node)\n",
    "builder.add_edge(START, \"chat\")\n",
    "builder.add_edge(\"chat\", END)\n",
    "graph = builder.compile(store=store)\n",
    "\n",
    "# ── 演示 ──\n",
    "if __name__ == \"__main__\":\n",
    "    config = {\"configurable\": {\"thread_id\": \"thread-42\"}}\n",
    "\n",
    "    # 会话1: user_42 存入偏好\n",
    "    print(\"会话1: user_42\")\n",
    "    r1 = graph.invoke(\n",
    "        {\"messages\": [{\"role\": \"user\", \"content\": \"记住我喜欢 Python 和 LangGraph\"}]},\n",
    "        context=Context(user_id=\"user_42\"),\n",
    "        config=config,\n",
    "    )\n",
    "    print(r1[\"messages\"][-1].content)\n",
    "\n",
    "    # 会话2: 同一用户，记忆保留\n",
    "    config = {\"configurable\": {\"thread_id\": \"thread-42\"}}\n",
    "    print(\"\\n会话2: user_42（换 thread）\")\n",
    "    r2 = graph.invoke(\n",
    "        {\"messages\": [{\"role\": \"user\", \"content\": \"我之前说过我喜欢什么？\"}]},\n",
    "        context=Context(user_id=\"user_42\"),\n",
    "        config=config\n",
    "    )\n",
    "    print(r2[\"messages\"][-1].content)\n",
    "\n",
    "    # 会话3: 不同用户，记忆隔离\n",
    "    print(\"\\n会话3: user_99（不同用户）\")\n",
    "    r3 = graph.invoke(\n",
    "        {\"messages\": [{\"role\": \"user\", \"content\": \"我之前说过我喜欢什么？\"}]},\n",
    "        context=Context(user_id=\"user_99\"),\n",
    "    )\n",
    "    print(r3[\"messages\"][-1].content)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9dc115f0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "用户 premium_user_001 在会话 session-888 中搜索: DeepSeek 最新文档\n",
      "{'messages': [HumanMessage(content='搜索 DeepSeek 最新文档', additional_kwargs={}, response_metadata={}, id='35bd6c7c-fe2c-437f-8c44-6abd83ac3fe4'), AIMessage(content='', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 337, 'prompt_tokens': 270, 'total_tokens': 607, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 304, 'rejected_prediction_tokens': None, 'text_tokens': 337}, 'prompt_tokens_details': {'audio_tokens': None, 'cache_write_tokens': None, 'cached_tokens': None, 'text_tokens': 270}}, 'model_provider': 'openai', 'model_name': 'qwen3.6-plus', 'system_fingerprint': None, 'id': 'chatcmpl-ca9de8c5-1c29-9007-98eb-d5fa38bb7948', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019f92c3-a209-75b3-bdc1-287313f00ac8-0', tool_calls=[{'name': 'search_database', 'args': {'query': 'DeepSeek 最新文档'}, 'id': 'call_4d2bf7c29dfe4a579614b117', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 270, 'output_tokens': 337, 'total_tokens': 607, 'input_token_details': {}, 'output_token_details': {'reasoning': 304}}), ToolMessage(content='content=\"\\'DeepSeek 最新文档\\' 的搜索结果（用户 premium_user_001）\" name=\\'search_database\\' tool_call_id=\\'call_4d2bf7c29dfe4a579614b117\\'', id='a031c380-250c-476c-b2d2-b0bf0dccdd7f', tool_call_id='call_4d2bf7c29dfe4a579614b117'), AIMessage(content='已为您执行搜索。系统返回的数据库结果为占位信息，建议您直接访问 DeepSeek 官方渠道获取最新、最准确的文档：\\n\\n📖 **官方文档与资源**\\n- **开发者平台 / API 文档**：`https://platform.deepseek.com/docs`（含 API 参考、鉴权方式、调用示例、计费说明等）\\n- **GitHub 开源仓库**：`https://github.com/deepseek-ai`（包含模型权重、推理代码、技术报告及微调指南）\\n- **官方博客与公告**：`https://www.deepseek.com` 或关注其官方技术博客，可获取版本更新与新功能说明\\n\\n💡 **文档常见核心模块**\\n- API 接入规范（Endpoint、Headers、Rate Limit）\\n- 模型能力对比（上下文长度、支持语言、函数调用、JSON Mode 等）\\n- SDK 快速开始（Python / Node.js / cURL 示例）\\n- 最佳实践与安全合规指南\\n\\n如果您需要查询某个具体功能（如：如何发起对话请求、如何设置温度/Top-P、如何接入流式输出、计费规则等），请告诉我具体场景，我将为您提取对应文档要点并提供可直接运行的代码示例。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 976, 'prompt_tokens': 371, 'total_tokens': 1347, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 712, 'rejected_prediction_tokens': None, 'text_tokens': 976}, 'prompt_tokens_details': {'audio_tokens': None, 'cache_write_tokens': None, 'cached_tokens': None, 'text_tokens': 371}}, 'model_provider': 'openai', 'model_name': 'qwen3.6-plus', 'system_fingerprint': None, 'id': 'chatcmpl-3038e249-ef81-9d78-85f1-adb59e460a96', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019f92c3-bcaa-7bb0-9a9a-10e0871ca1e0-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 371, 'output_tokens': 976, 'total_tokens': 1347, 'input_token_details': {}, 'output_token_details': {'reasoning': 712}})]}\n"
     ]
    }
   ],
   "source": [
    "from langgraph.graph import StateGraph, MessagesState, START, END\n",
    "from langchain_core.runnables import RunnableConfig\n",
    "from langchain_core.tools import tool\n",
    "from langchain_core.messages import ToolMessage\n",
    "from dotenv import load_dotenv\n",
    "import os\n",
    "load_dotenv()\n",
    "\n",
    "# ── 工具：config 由 LangGraph 自动注入 ──\n",
    "@tool\n",
    "def search_database(query: str, config: RunnableConfig) -> str:\n",
    "    \"\"\"搜索数据库\"\"\"\n",
    "    cfg = config.get(\"configurable\", {})\n",
    "    user_id = cfg.get(\"user_id\", \"anonymous\")\n",
    "    thread_id = cfg.get(\"thread_id\")\n",
    "    print(f\"用户 {user_id} 在会话 {thread_id} 中搜索: {query}\")\n",
    "    return f\"'{query}' 的搜索结果（用户 {user_id}）\"\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",
    ").bind_tools([search_database])\n",
    "\n",
    "# ── 模型 ──\n",
    "# model = ChatOpenAI(\n",
    "#     model=\"deepseek-v4-flash\",\n",
    "#     api_key=api_key,\n",
    "#     base_url=\"https://api.deepseek.com\",\n",
    "# ).bind_tools([search_database])\n",
    "\n",
    "\n",
    "tools_map = {\"search_database\": search_database}\n",
    "\n",
    "# ── 节点 ──\n",
    "def call_model(state: MessagesState):\n",
    "    response = model.invoke(state[\"messages\"])\n",
    "    return {\"messages\": [response]}\n",
    "\n",
    "def call_tools(state: MessagesState, config: RunnableConfig):\n",
    "    last_msg = state[\"messages\"][-1]\n",
    "    results = []\n",
    "    for tc in last_msg.tool_calls:\n",
    "        fn = tools_map[tc[\"name\"]]\n",
    "        results.append(ToolMessage(content=fn.invoke(tc, config=config), tool_call_id=tc[\"id\"]))\n",
    "    return {\"messages\": results}\n",
    "\n",
    "def route(state: MessagesState):\n",
    "    last_msg = state[\"messages\"][-1]\n",
    "    return \"call_tools\" if hasattr(last_msg, \"tool_calls\") and last_msg.tool_calls else END\n",
    "\n",
    "# ── 构图 & 运行 ──\n",
    "builder = StateGraph(MessagesState)\n",
    "\n",
    "builder.add_node(\"call_model\", call_model)\n",
    "builder.add_node(\"call_tools\", call_tools)\n",
    "\n",
    "builder.add_edge(START, \"call_model\")\n",
    "builder.add_conditional_edges(\"call_model\", route, {\"call_tools\": \"call_tools\", END: END})\n",
    "builder.add_edge(\"call_tools\", \"call_model\")\n",
    "\n",
    "graph = builder.compile()\n",
    "\n",
    "result = graph.invoke(\n",
    "    {\"messages\": [{\"role\": \"user\", \"content\": \"搜索 DeepSeek 最新文档\"}]},\n",
    "    config={\"configurable\": {\"thread_id\": \"session-888\", \"user_id\": \"premium_user_001\"}},\n",
    ")\n",
    "\n",
    "print(result)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1b5b6f60",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 第一轮：查询天气 ===\n",
      "成都今天天气晴朗，气温较高，达到了 **36°C**，湿度为 37%，风速 6 km/h。空气质量方面，AQI 为 58，等级为 **良**。\n",
      "\n",
      "关于是否适合出门跑步，建议如下：\n",
      "\n",
      "虽然空气质量不错（良），适合户外活动，但 **36°C 的气温过高**，在白天进行户外跑步容易导致中暑或脱水，体感会非常不适。\n",
      "\n",
      "**建议：**\n",
      "*   **避开高温时段**：如果一定要跑，请尽量选择在 **清晨** 或 **傍晚/夜间** 气温较低的时候进行。\n",
      "*   **注意防暑补水**：高温下运动出汗量大，务必携带充足的水，并注意防晒。\n",
      "*   **考虑室内运动**：如果无法避开白天时段，建议改为在室内健身房或有空调的环境下运动。\n",
      "\n",
      "=== 第二轮：追问空气质量 ===\n",
      "成都当前的空气质量指数（AQI）为 **58**，等级为 **良**。\n",
      "\n",
      "这意味着空气质量总体良好，适宜进行户外活动。对于跑步等运动来说，空气质量是一个有利条件，您不必担心空气污染对呼吸道的影响（主要需注意的仍是之前提到的高温防暑问题）。\n",
      "\n",
      "=== 第三轮：测试记忆 ===\n",
      "您刚才问的是**成都**。\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 11248 characters]",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.tools import tool\n",
    "from langchain_core.messages import HumanMessage, ToolMessage\n",
    "from langgraph.graph import StateGraph, MessagesState, START, END\n",
    "from langgraph.prebuilt import ToolNode\n",
    "from langgraph.checkpoint.memory import InMemorySaver\n",
    "import requests\n",
    "from typing import Literal\n",
    "\n",
    "\n",
    "# ============================================================\n",
    "# 步骤 1：初始化 DeepSeek 模型\n",
    "# ============================================================\n",
    "# model = ChatOpenAI(\n",
    "#     model=\"deepseek-v4-flash\",\n",
    "#     api_key=api_key,    # ← 替换为你的 API Key\n",
    "#     base_url=\"https://api.deepseek.com\",\n",
    "#     temperature=0.7,                    # 天气查询不需要太高创造性\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",
    "    temperature=0.7,                    # 天气查询不需要太高创造性\n",
    ")\n",
    "\n",
    "# ============================================================\n",
    "# 步骤 2：定义工具函数\n",
    "# ============================================================\n",
    "@tool\n",
    "def get_weather(city: str) -> str:\n",
    "    \"\"\"\n",
    "    查询指定城市的实时天气。\n",
    "\n",
    "    参数:\n",
    "        city: 城市名称，支持中文或英文，如 \"北京\"、\"Shanghai\"\n",
    "\n",
    "    返回:\n",
    "        包含天气描述和温度的字符串\n",
    "    \"\"\"\n",
    "    url = f\"https://wttr.in/{city}?format=j1&lang=zh\"\n",
    "    try:\n",
    "        resp = requests.get(url, timeout=10)\n",
    "        resp.raise_for_status()    # HTTP 错误就抛异常\n",
    "        data = resp.json()\n",
    "\n",
    "        current = data[\"current_condition\"][0]\n",
    "        desc = current[\"weatherDesc\"][0][\"value\"]   # 天气描述，如\"晴\"\n",
    "        temp = current[\"temp_C\"]                     # 当前温度（摄氏度）\n",
    "        humidity = current[\"humidity\"]               # 湿度\n",
    "        wind = current[\"windspeedKmph\"]              # 风速\n",
    "\n",
    "        return (\n",
    "            f\"城市：{city}\\n\"\n",
    "            f\"天气：{desc}\\n\"\n",
    "            f\"温度：{temp}°C\\n\"\n",
    "            f\"湿度：{humidity}%\\n\"\n",
    "            f\"风速：{wind} km/h\"\n",
    "        )\n",
    "    except Exception as e:\n",
    "        return f\"查询 {city} 天气失败：{str(e)}\"\n",
    "\n",
    "\n",
    "@tool\n",
    "def get_air_quality(city: str) -> str:\n",
    "    \"\"\"\n",
    "    查询指定城市的空气质量指数（AQI）。\n",
    "\n",
    "    参数:\n",
    "        city: 城市名称，如 \"北京\"\n",
    "\n",
    "    返回:\n",
    "        AQI 数值和等级描述\n",
    "    \"\"\"\n",
    "    # 这里用模拟数据演示；实际项目中替换为真实 AQI API\n",
    "    import random\n",
    "    aqi = random.randint(20, 250)\n",
    "    if aqi <= 50:\n",
    "        level = \"优\"\n",
    "    elif aqi <= 100:\n",
    "        level = \"良\"\n",
    "    elif aqi <= 150:\n",
    "        level = \"轻度污染\"\n",
    "    elif aqi <= 200:\n",
    "        level = \"中度污染\"\n",
    "    else:\n",
    "        level = \"重度污染\"\n",
    "    return f\"{city} 当前 AQI：{aqi}（{level}）\"\n",
    "\n",
    "\n",
    "# ============================================================\n",
    "# 步骤 3：将工具绑定到模型\n",
    "# ============================================================\n",
    "tools = [get_weather, get_air_quality]\n",
    "\n",
    "# bind_tools 让 DeepSeek 知道有哪些工具可用\n",
    "# 模型回复时会自动返回 tool_calls（如果需要调用工具）或普通文本\n",
    "model_with_tools = model.bind_tools(tools)\n",
    "\n",
    "\n",
    "# ============================================================\n",
    "# 步骤 4：定义图节点\n",
    "# ============================================================\n",
    "def agent_node(state: MessagesState) -> dict:\n",
    "    \"\"\"\n",
    "    Agent 节点：调用大模型。\n",
    "    模型会根据用户问题和绑定的工具，决定是直接回答还是调用工具。\n",
    "    \"\"\"\n",
    "    response = model_with_tools.invoke(state[\"messages\"])\n",
    "    # 返回的消息通过 add_messages Reducer 自动追加到历史\n",
    "    return {\"messages\": [response]}\n",
    "\n",
    "\n",
    "# 不需要手写工具调用逻辑！ToolNode 自动处理\n",
    "# ToolNode 会读取最后一条消息的 tool_calls，逐个执行工具，返回 ToolMessage 列表\n",
    "tool_node = ToolNode(tools)\n",
    "\n",
    "\n",
    "def router(state: MessagesState) -> Literal[\"tools\", END]:\n",
    "    \"\"\"\n",
    "    路由函数：检查模型是否想调用工具。\n",
    "\n",
    "    - 如果最后一条消息包含 tool_calls → 走工具节点\n",
    "    - 如果只是普通回复 → 结束，返回给用户\n",
    "    \"\"\"\n",
    "    last_msg = state[\"messages\"][-1]\n",
    "    # AIMessage 有 tool_calls 属性，且列表非空 → 模型想调工具\n",
    "    if hasattr(last_msg, \"tool_calls\") and last_msg.tool_calls:\n",
    "        return \"tools\"\n",
    "    return END\n",
    "\n",
    "\n",
    "# ============================================================\n",
    "# 步骤 5：构建并编译图\n",
    "# ============================================================\n",
    "builder = StateGraph(MessagesState)\n",
    "\n",
    "# 注册节点\n",
    "builder.add_node(\"agent\", agent_node)     # LLM 思考节点\n",
    "builder.add_node(\"tools\", tool_node)      # 工具执行节点\n",
    "\n",
    "# 连接边\n",
    "builder.set_entry_point(\"agent\")          # 从 agent 开始\n",
    "builder.add_conditional_edges(            # agent → router → tools 或 END\n",
    "    \"agent\",\n",
    "    router,\n",
    "    {\"tools\": \"tools\", END: END},\n",
    ")\n",
    "builder.add_edge(\"tools\", \"agent\")        # 工具结果 → 回到 agent 继续思考\n",
    "\n",
    "# 编译（带记忆）\n",
    "graph = builder.compile(checkpointer=InMemorySaver())\n",
    "\n",
    "\n",
    "# ============================================================\n",
    "# 步骤 6：执行\n",
    "# ============================================================\n",
    "def chat_once(user_input: str, thread_id: str = \"default\") -> str:\n",
    "    \"\"\"封装一次对话\"\"\"\n",
    "    config = {\"configurable\": {\"thread_id\": thread_id}}\n",
    "    result = graph.invoke(\n",
    "        {\"messages\": [HumanMessage(content=user_input)]},\n",
    "        config=config,\n",
    "    )\n",
    "    return result[\"messages\"][-1].content\n",
    "\n",
    "\n",
    "# 测试：查询天气\n",
    "print(\"=== 第一轮：查询天气 ===\")\n",
    "print(chat_once(\"成都今天的天气怎么样？适合出门跑步吗？\", thread_id=\"user-001\"))\n",
    "\n",
    "print(\"\\n=== 第二轮：追问空气质量 ===\")\n",
    "print(chat_once(\"成都的空气质量如何？\", thread_id=\"user-001\"))\n",
    "\n",
    "print(\"\\n=== 第三轮：测试记忆 ===\")\n",
    "print(chat_once(\"我刚才问的是哪个城市？\", thread_id=\"user-001\"))\n",
    "\n",
    "#显示图结构\n",
    "from IPython.display import Image,display\n",
    "display(Image(graph.get_graph().draw_mermaid_png()))"
   ]
  }
 ],
 "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
}
