{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "38b007c8",
   "metadata": {},
   "source": [
    "\n",
    "## GiftGeniusAgent——你的送礼智能Agent\n",
    "\n",
    "### 项目简介\n",
    "本项目演示一个基于HelloAgents框架的智能送礼Agent\n",
    "\n",
    "### 作者信息\n",
    "- 姓名：张善祺\n",
    "- GitHub:@jack6249\n",
    "- 日期：2025-11-21\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "62a3b6a3",
   "metadata": {},
   "source": [
    "### 第1部分：环境配置"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dd9b8a20",
   "metadata": {},
   "outputs": [],
   "source": [
    "#导入库和参数配置\n",
    "from hello_agents import SimpleAgent, HelloAgentsLLM, ReflectionAgent, ToolRegistry\n",
    "from hello_agents.tools import Tool, ToolParameter\n",
    "from typing import Dict, Any, List\n",
    "from tavily import TavilyClient\n",
    "import os\n",
    "import json\n",
    "import re\n",
    "import numpy as np \n",
    "from dotenv import load_dotenv\n",
    "import asyncio\n",
    "import nest_asyncio\n",
    "from mcp.client.sse import sse_client\n",
    "from mcp.client.session import ClientSession\n",
    "\n",
    "load_dotenv()\n",
    "\n",
    "#LLM参数\n",
    "LLM_MODEL_ID = os.getenv(\"LLM_MODEL_ID\")\n",
    "LLM_API_KEY = os.getenv(\"LLM_API_KEY\")\n",
    "LLM_BASE_URL = os.getenv(\"LLM_BASE_URL\")\n",
    "#Tavily参数\n",
    "TAVILY_API_KEY = os.getenv(\"TAVILY_API_KEY\",\"\")\n",
    "#百度MCP参数\n",
    "BAIDU_TOKEN = os.getenv(\"BAIDU_MCP_TOKEN\",\"\")\n",
    "#输入json路径配置\n",
    "INPUT_FILENAME = \"data/test_cases.json\"\n",
    "\n",
    "# 搜索源配置\n",
    "# 可选值: \"tavily\" (通用/海外) 或 \"baidu\" (电商/国内)\n",
    "os.environ[\"SEARCH_PROVIDER\"] = \"baidu\" \n",
    "\n",
    "print(\"✅ 环境配置完成\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9404c299",
   "metadata": {},
   "source": [
    "### 第2部分：定义工具"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "37b994bc",
   "metadata": {},
   "outputs": [],
   "source": [
    "# [Cell 2 终极版] 定义统一搜索工具 (兼容 Tavily 和 Baidu)\n",
    "# 允许 Jupyter 运行异步\n",
    "nest_asyncio.apply()\n",
    "\n",
    "class BatchSearchTool(Tool):\n",
    "    def __init__(self):\n",
    "        super().__init__(\n",
    "            name=\"batch_search\",\n",
    "            description=\"统一搜索工具，支持 Tavily 和 Baidu 切换。\"\n",
    "        )\n",
    "        self.provider = os.environ.get(\"SEARCH_PROVIDER\", \"tavily\").lower()\n",
    "\n",
    "    def run(self, parameters: Any) -> str:\n",
    "        return \"请使用 Python 代码直接调用 search_raw 方法获取数据。\"\n",
    "\n",
    "    def search_raw(self, query: str) -> List[Dict]:\n",
    "        if self.provider == \"baidu\":\n",
    "            return self._search_baidu(query)\n",
    "        else:\n",
    "            return self._search_tavily(query)\n",
    "\n",
    "    # --- 引擎 A: Tavily ---\n",
    "    def _search_tavily(self, query: str) -> List[Dict]:\n",
    "        api_key = os.environ.get(\"TAVILY_API_KEY\")\n",
    "        if not api_key: return []\n",
    "        print(f\"    🚀 [Tavily] 正在搜索: {query} ...\")\n",
    "        try:\n",
    "            tavily = TavilyClient(api_key=api_key)\n",
    "            response = tavily.search(query, max_results=5, include_images=True)\n",
    "            results = []\n",
    "            if 'results' in response:\n",
    "                for r in response['results']:\n",
    "                    results.append({\n",
    "                        \"title\": r['title'], \"url\": r['url'], \"content\": r['content'], \n",
    "                        \"type\": \"text\", \"img\": \"\" # Tavily 文本通常不带图\n",
    "                    })\n",
    "            if 'images' in response and response['images']:\n",
    "                results.append({\"images\": response['images'][:3], \"type\": \"image\"})\n",
    "            return results\n",
    "        except Exception as e:\n",
    "            print(f\"      ⚠️ Tavily 异常: {e}\")\n",
    "            return []\n",
    "\n",
    "    # --- 引擎 B: Baidu MCP ---\n",
    "    def _search_baidu(self, query: str) -> List[Dict]:\n",
    "        token = os.environ.get(\"BAIDU_MCP_TOKEN\")\n",
    "        if not token: return []\n",
    "        print(f\"    🐼 [百度优选] 正在搜索: {query} ...\")\n",
    "        try:\n",
    "            raw_json_str = asyncio.run(self._async_baidu_call(query, token))\n",
    "            print(f\"      🔍 原始 JSON 响应: {raw_json_str}\")\n",
    "            return self._parse_baidu_response(raw_json_str)\n",
    "        except Exception as e:\n",
    "            print(f\"      ⚠️ 百度 MCP 异常: {e}\")\n",
    "            return []\n",
    "\n",
    "    async def _async_baidu_call(self, query: str, token: str) -> str:\n",
    "        sse_url = f\"https://mcp-youxuan.baidu.com/mcp/sse?key={token}\"\n",
    "        async with sse_client(sse_url) as (read, write):\n",
    "            async with ClientSession(read, write) as session:\n",
    "                await session.initialize()\n",
    "                result = await session.call_tool(\"goods_search\", arguments={\"query\": query})\n",
    "                return result.content[0].text if result.content else \"\"\n",
    "\n",
    "    def _parse_baidu_response(self, json_str: str) -> List[Dict]:\n",
    "        results = []; images = []\n",
    "        try:\n",
    "            data = json.loads(json_str)\n",
    "            items = data if isinstance(data, list) else []\n",
    "            \n",
    "            for item in items[:5]:\n",
    "                title = item.get(\"goodsName\") or item.get(\"title\") or \"未知商品\"\n",
    "                price = item.get(\"price\") or item.get(\"minPrice\") or \"\"\n",
    "                shop = item.get(\"shopName\") or item.get(\"mall\") or \"\"\n",
    "                url = item.get(\"detailUrl\") or item.get(\"url\") or item.get(\"ori_url\") or \"#\"\n",
    "                img = item.get(\"imgUrl\") or item.get(\"picUrl\") or item.get(\"img\")\n",
    "                \n",
    "                content = f\"价格: {price}元。店铺: {shop}。商品详情: {title}\"\n",
    "                \n",
    "                # 📝【修复点】直接在 text 类型结果里绑定 img\n",
    "                results.append({\n",
    "                    \"title\": title, \"url\": url, \"content\": content, \n",
    "                    \"type\": \"text\", \"img\": img \n",
    "                })\n",
    "                if img: images.append(img)\n",
    "            \n",
    "            if images: results.append({\"images\": images[:3], \"type\": \"image\"})\n",
    "                \n",
    "        except json.JSONDecodeError:\n",
    "            print(\"      ⚠️ 百度返回非 JSON 数据\")\n",
    "        return results\n",
    "\n",
    "    def get_parameters(self):\n",
    "        return [ToolParameter(name=\"query\", type=\"string\", description=\"关键词\")]\n",
    "\n",
    "tool_registry = ToolRegistry()\n",
    "tool_registry.register_tool(BatchSearchTool())\n",
    "\n",
    "print(\"✅ 统一搜索工具已加载！\")\n",
    "print(f\"当前模式: {'百度优选 (电商)' if os.environ.get('SEARCH_PROVIDER') == 'baidu' else 'Tavily (通用)'}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e8ffecb0",
   "metadata": {},
   "source": [
    "### 第3部分：创建智能体\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6c5a2e82",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 初始化大模型\n",
    "llm = HelloAgentsLLM()\n",
    "\n",
    "# --- 1. 军师 (Profiler) - 已升级支持多维度画像 ---\n",
    "PROFILER_PROMPT = \"\"\"\n",
    "你是一个精通 MBTI 人格分析与消费市场趋势的 \"送礼军师\"。\n",
    "你的任务是根据用户提供的多维度画像，制定 3 个**极度精准**的搜索关键词。\n",
    "\n",
    "【⚠️ 时效性死命令 (CRITICAL)】\n",
    "当前时间视作 **2025年11月**。\n",
    "1. **严禁过时**：绝对不要推荐 2024 年或更早的旧款（除非是经典恒久款如黑胶唱片）。\n",
    "2. **价格尺度**：给出的价格单位是人民币元。请严格遵照范围进行联想，禁止超出预算范围。\n",
    "\n",
    "为了确保推荐质量，请参考以下的【优秀思考范例】：\n",
    "\n",
    "### 范例 1\n",
    "**用户画像**: \n",
    "- 女, 26岁, ISFP (探险家), 金牛座\n",
    "- 预算: 500-1000元\n",
    "- 场景: 情人节\n",
    "- 自定义: 喜欢有质感的生活小物\n",
    "**军师分析**: \n",
    "ISFP 重视审美和感官体验，金牛座喜欢实实在在的质感。情人节需要浪漫。\n",
    "**生成策略**:\n",
    "1. 观夏 (To Summer) 昆仑煮雪 晶石香薰 (符合质感与审美)\n",
    "2. 野兽派 2025 情人节限定 睡衣礼盒 (金牛座喜欢的舒适)\n",
    "3. 富士 Instax mini Evo 拍立得 (记录生活瞬间)\n",
    "\n",
    "### 范例 2\n",
    "**用户画像**: \n",
    "- 男, 30岁, INTJ (建筑师), 处女座\n",
    "- 预算: 1000元以上\n",
    "- 场景: 生日\n",
    "- 自定义: 程序员，喜欢整洁\n",
    "**军师分析**: \n",
    "INTJ 追求极致的逻辑和效率，处女座有洁癖，喜欢桌面整洁。\n",
    "**生成策略**:\n",
    "1. Keychron Q1 Pro 机械键盘 铝坨坨 (符合极客对工具的追求)\n",
    "2. 明基 (BenQ) ScreenBar Halo 屏幕挂灯 (极致护眼与桌面美学)\n",
    "3. 赫曼米勒 (Herman Miller) 显示器支架 (人体工学)\n",
    "\n",
    "### 范例 3\n",
    "**用户画像**: \n",
    "- 女, 20岁, ENFP (竞选者), 狮子座\n",
    "- 预算: 300元以内\n",
    "- 场景: 圣诞节\n",
    "- 自定义: 喜欢二次元，痛包\n",
    "**军师分析**: \n",
    "ENFP 热情奔放，狮子座喜欢张扬、闪亮的东西。预算有限但要素多。\n",
    "**生成策略**:\n",
    "1. 泡泡玛特 圣诞系列 盲盒整端 (符合节日气氛和二次元)\n",
    "2. WEGO 痛包 镭射款 (符合自定义需求，狮子座喜欢的亮眼)\n",
    "3.  Chiikawa 吉伊卡哇 圣诞公仔 (当下顶流二次元IP)\n",
    "\n",
    "---\n",
    "\n",
    "**现在的任务**：\n",
    "请根据以下【当前用户画像】进行分析，模仿上述范例的深度，制定搜索策略。\n",
    "\n",
    "【当前用户画像】\n",
    "{user_profile_text}\n",
    "\n",
    "【关键词生成要求】\n",
    "1. **必须具体**：格式为 `[品牌] + [产品名/系列] + [限定/属性]`。\n",
    "2. **拒绝大词**：严禁搜索 \"礼物\"、\"口红\"、\"玩具\" 这种泛词。\n",
    "3. **必须包含品牌**：根据预算推断合适的品牌（如：预算低选名创优品/泡泡玛特，预算高选Dior/索尼）。\n",
    "\n",
    "【输出格式】\n",
    "只输出 3 行搜索关键词，每行一个。不要输出分析过程，不要序号。\n",
    "\"\"\"\n",
    "\n",
    "profiler_agent = SimpleAgent(\n",
    "    llm=llm,\n",
    "    name=\"Agent_Profiler\",\n",
    "    system_prompt=PROFILER_PROMPT\n",
    ")\n",
    "\n",
    "# ==============================================================================\n",
    "# 2. 种草达人 (Pitcher) - 文案创作 (加入风格指导)\n",
    "# ==============================================================================\n",
    "PITCHER_PROMPT = \"\"\"\n",
    "你是一个 **金牌种草文案**。\n",
    "用户会给你一个 **【商品名称】**。\n",
    "\n",
    "### 🎯 关键要点 (Few Points)\n",
    "1.  **痛点直击**: 一句话说清楚为什么买它（限定？显白？绝美？）。\n",
    "2.  **情绪价值**: 使用 \"绝绝子\", \"氛围感\", \"心动\" 等高频热词。\n",
    "3.  **字数限制**: 严格控制在 **40字以内**，短小精悍。\n",
    "4.  **Emoji**: 必须包含 1-2 个 emoji。\n",
    "\n",
    "### 🌟 创作范例 (Few-Shot)\n",
    "**输入**: Dior 999 烈艳蓝金\n",
    "**输出**: 💄本宫不死终是妃！Dior 999 传奇正红，显白更有气场，送女友绝对没错！\n",
    "\n",
    "**输入**: 泡泡玛特 Labubu 坐坐派对\n",
    "**输出**: ✨太可爱了吧！Labubu 坐坐派对系列，每一个都丑萌到心巴上，摆在桌上超治愈~\n",
    "\n",
    "**输入**: 罗技 MX Master 3S\n",
    "**输出**: 🖱️打工人本命！罗技 Master 3S 静音又顺滑，人体工学设计，手腕再也不累了。\n",
    "\n",
    "---\n",
    "\n",
    "**当前任务**：\n",
    "请为【{input}】写一句朋友圈风格种草语。\n",
    "\"\"\"\n",
    "pitcher_agent = SimpleAgent(\n",
    "    llm=llm,\n",
    "    name=\"Agent_Pitcher\",\n",
    "    system_prompt=PITCHER_PROMPT\n",
    ")\n",
    "\n",
    "print(\"✅ 智能体初始化完成！\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "25907c14",
   "metadata": {},
   "source": [
    "### 第4部分:读取数据"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "58bd39dd",
   "metadata": {},
   "outputs": [],
   "source": [
    "def load_user_profile(filename):\n",
    "    # 1. 检查文件是否存在\n",
    "    if not os.path.exists(filename):\n",
    "        print(f\"⚠️ 未找到配置文件: {filename}\")\n",
    "        # 如果没有文件，将默认数据写入文件\n",
    "        default_data = {\n",
    "            \"性别\": \"女\",\n",
    "            \"年龄\": \"24岁\",\n",
    "            \"MBTI\": \"ENFP\",\n",
    "            \"星座\": \"天秤座\",\n",
    "            \"预算\": \"500元以内\",\n",
    "            \"节日\": \"恋爱一周年纪念日\",\n",
    "            \"自定义\": \"喜欢二次元，平时喜欢喝咖啡，不要送太实用的家电\"\n",
    "        }\n",
    "        with open(filename, \"w\", encoding=\"utf-8\") as f:\n",
    "            json.dump(default_data, f, ensure_ascii=False, indent=4)\n",
    "        print(f\"✅ 已自动生成默认配置文件，请修改 {filename} 后再次运行。\")\n",
    "        return default_data\n",
    "\n",
    "    # 2. 读取文件内容\n",
    "    try:\n",
    "        with open(filename, \"r\", encoding=\"utf-8\") as f:\n",
    "            data = json.load(f)\n",
    "        print(f\"✅ 成功加载用户画像: {filename}\")\n",
    "        print(f\"📋 内容预览: {json.dumps(data, ensure_ascii=False)}\")\n",
    "        return data\n",
    "    except Exception as e:\n",
    "        print(f\"❌ 读取 JSON 失败: {e}\")\n",
    "        return {}\n",
    "\n",
    "# 加载数据\n",
    "user_input_data = load_user_profile(INPUT_FILENAME)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0fdcab5a",
   "metadata": {},
   "source": [
    "### 第5部分：生成礼物计划"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cdaef6b1",
   "metadata": {},
   "outputs": [],
   "source": [
    "def parse_budget_range(budget_str):\n",
    "    \"\"\"解析用户预算字符串，返回 (min, max)\"\"\"\n",
    "    nums = [float(x) for x in re.findall(r'\\d+', str(budget_str).replace(',', ''))]\n",
    "    if not nums: return 0, 999999 \n",
    "    if \"以内\" in budget_str or \"以下\" in budget_str: return 0, nums[0]\n",
    "    if \"以上\" in budget_str: return nums[0], 999999\n",
    "    if len(nums) >= 2: return min(nums), max(nums)\n",
    "    return 0, nums[0]\n",
    "\n",
    "def extract_all_prices(raw_results):\n",
    "    \"\"\"从搜索结果列表中提取所有有效的价格\"\"\"\n",
    "    prices = []\n",
    "    for res in raw_results:\n",
    "        # 只处理文本类型的结果\n",
    "        if res.get('type') == 'text':\n",
    "            text = res.get('title', '') + \" \" + res.get('content', '')\n",
    "            # 匹配 ¥, $, 元 等格式\n",
    "            matches = re.findall(r'(?:¥|￥|\\$|HK\\$|NT\\$)\\s*(\\d+(?:,\\d{3})*(?:\\.\\d+)?)', text)\n",
    "            for m in matches:\n",
    "                val = float(m.replace(',', ''))\n",
    "                # 过滤掉像年份(2025)或过小/过大的异常值\n",
    "                if 10 < val < 100000 and val not in [2024, 2025, 2026]:\n",
    "                    prices.append(val)\n",
    "            # 备用正则：匹配 \"xxx元\"\n",
    "            matches_yuan = re.findall(r'(\\d+(?:,\\d{3})*(?:\\.\\d+)?)\\s*元', text)\n",
    "            for m in matches_yuan:\n",
    "                val = float(m.replace(',', ''))\n",
    "                if 10 < val < 100000 and val not in [2024, 2025, 2026]:\n",
    "                    prices.append(val)\n",
    "    return prices\n",
    "\n",
    "\n",
    "def find_best_product(hunter, profiler_agent, keyword, budget_min, budget_max):\n",
    "    limit_upper = budget_max * 1.2\n",
    "    limit_lower = budget_min * 0.8\n",
    "    \n",
    "    all_candidates = []\n",
    "    current_kw = keyword\n",
    "    \n",
    "    # --- Round 1: 首次搜索 ---\n",
    "    print(f\"       🕵️ 第1次搜索: {current_kw} 价格\")\n",
    "    results_1 = hunter.search_raw(f\"{current_kw} 价格\")\n",
    "    \n",
    "    fallback_img = \"\"\n",
    "    for r in results_1:\n",
    "        if r.get('images'): \n",
    "            fallback_img = r['images'][0]\n",
    "            break\n",
    "\n",
    "    has_valid_info = False\n",
    "    for res in results_1:\n",
    "        if res.get('type') == 'text':\n",
    "            has_valid_info = True\n",
    "            p_vals = extract_all_prices([res])\n",
    "            if p_vals:\n",
    "                res['price_val'] = p_vals[0]\n",
    "                # 📝【核心修复点1】记录当前结果所属的关键词\n",
    "                res['source_kw'] = current_kw \n",
    "                all_candidates.append(res)\n",
    "                \n",
    "                if limit_lower <= p_vals[0] <= limit_upper:\n",
    "                    if not res.get('img') and fallback_img: res['img'] = fallback_img\n",
    "                    return res, f\"约 {p_vals[0]}元\", current_kw\n",
    "\n",
    "    # --- 机制 3: 无数据防御 ---\n",
    "    if not has_valid_info:\n",
    "        print(f\"       ⚠️ [机制3触发] 首次搜索无有效信息。\")\n",
    "        correction_prompt = f\"原策略 '{current_kw}' 搜索结果为空，请推荐一个同品类但更热门的具体商品型号。只输出关键词。\"\n",
    "        new_kw = profiler_agent.run(correction_prompt).strip()\n",
    "        print(f\"       🔄 军师换词: {new_kw}\")\n",
    "        current_kw = new_kw\n",
    "        \n",
    "        results = hunter.search_raw(f\"{current_kw} 价格\")\n",
    "        \n",
    "        # 更新 fallback_img\n",
    "        fallback_img = \"\" \n",
    "        for r in results:\n",
    "            if r.get('images'): \n",
    "                fallback_img = r['images'][0]\n",
    "                break\n",
    "                \n",
    "        for res in results:\n",
    "            if res.get('type') == 'text':\n",
    "                p_vals = extract_all_prices([res])\n",
    "                if p_vals:\n",
    "                    res['price_val'] = p_vals[0]\n",
    "                    # 📝【核心修复点1】记录关键词\n",
    "                    res['source_kw'] = current_kw\n",
    "                    all_candidates.append(res)\n",
    "\n",
    "    # --- 机制 1 & 2: 价格修正 ---\n",
    "    avg_price = np.mean([c['price_val'] for c in all_candidates]) if all_candidates else 0\n",
    "    \n",
    "    if avg_price > 0:\n",
    "        correction_prompt = \"\"\n",
    "        if avg_price > limit_upper:\n",
    "            print(f\"       💸 [机制1触发] 均价 {int(avg_price)} > 上限 {int(limit_upper)}，找平替...\")\n",
    "            correction_prompt = f\"原策略 '{current_kw}' 均价约 {int(avg_price)}元，超预算 ({budget_max}元)。请推荐一个同品类更便宜的具体型号（平替）。只输出关键词。\"\n",
    "        elif avg_price < limit_lower:\n",
    "            print(f\"       📉 [机制2触发] 均价 {int(avg_price)} < 下限 {int(limit_lower)}，找升级款...\")\n",
    "            correction_prompt = f\"原策略 '{current_kw}' 均价约 {int(avg_price)}元，低于预算下限 ({budget_min}元)。请推荐一个同品类更高端的型号。只输出关键词。\"\n",
    "            \n",
    "        if correction_prompt:\n",
    "            new_kw = profiler_agent.run(correction_prompt).strip()\n",
    "            print(f\"       🔄 军师修正: {new_kw}\")\n",
    "            current_kw = new_kw\n",
    "            \n",
    "            results_2 = hunter.search_raw(f\"{new_kw} 价格\")\n",
    "            \n",
    "            # 更新 fallback_img\n",
    "            fallback_img = \"\" \n",
    "            for r in results_2:\n",
    "                if r.get('images'): \n",
    "                    fallback_img = r['images'][0]\n",
    "                    break\n",
    "            \n",
    "            for res in results_2:\n",
    "                if res.get('type') == 'text':\n",
    "                    p_vals = extract_all_prices([res])\n",
    "                    if p_vals:\n",
    "                        res['price_val'] = p_vals[0]\n",
    "                        # 📝【核心修复点1】记录关键词\n",
    "                        res['source_kw'] = current_kw\n",
    "                        all_candidates.append(res) \n",
    "                        \n",
    "                        if limit_lower <= p_vals[0] <= limit_upper:\n",
    "                            if not res.get('img') and fallback_img: res['img'] = fallback_img\n",
    "                            tag = \"(平替)\" if avg_price > limit_upper else \"(升级)\"\n",
    "                            return res, f\"约 {p_vals[0]}元 {tag}\", current_kw\n",
    "\n",
    "    # --- 机制 4: 兜底防御 ---\n",
    "    print(\"       ⚠️ [机制4触发] 启用强制兜底模式...\")\n",
    "    best_fallback = None\n",
    "    status_msg = \"暂无报价\"\n",
    "    \n",
    "    if all_candidates:\n",
    "        # 选离预算最近的\n",
    "        target = (budget_min + budget_max) / 2\n",
    "        best_fallback = sorted(all_candidates, key=lambda x: abs(x['price_val'] - target))[0]\n",
    "        p = best_fallback['price_val']\n",
    "        \n",
    "        if p > limit_upper: status_msg = f\"约 {p}元 (⚠️超预算)\"\n",
    "        elif p < limit_lower: status_msg = f\"约 {p}元 (📉低于预算)\"\n",
    "        else: status_msg = f\"约 {p}元\"\n",
    "        \n",
    "    elif results_1:\n",
    "        # 实在没数据，硬取第一条\n",
    "        for res in results_1:\n",
    "            if res.get('type') == 'text': \n",
    "                best_fallback = res\n",
    "                # 兜底时如果也没价格，就用原始关键词\n",
    "                best_fallback['source_kw'] = keyword \n",
    "                break\n",
    "    \n",
    "    if best_fallback:\n",
    "        if not best_fallback.get('img') and fallback_img:\n",
    "            best_fallback['img'] = fallback_img\n",
    "        \n",
    "        # 📝【核心修复点2】返回结果里记录的那个 source_kw，而不是当前的 current_kw\n",
    "        final_name_to_use = best_fallback.get('source_kw', current_kw)\n",
    "        \n",
    "        return best_fallback, status_msg, final_name_to_use\n",
    "        \n",
    "    return None, \"搜索失败\", keyword"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4ec82cc7",
   "metadata": {},
   "outputs": [],
   "source": [
    "if not user_input_data:\n",
    "    print(\"❌ 未加载用户数据\")\n",
    "else:\n",
    "    # 0. 解析预算\n",
    "    b_min, b_max = parse_budget_range(user_input_data.get('预算', ''))\n",
    "    print(f\"\\n💰 预算范围: {b_min} - {b_max}元\")\n",
    "\n",
    "    # 1. 军师制定策略\n",
    "    profile_text = \"\\n\".join([f\"- {k}: {v if v else '未知/不限'}\" for k, v in user_input_data.items()])\n",
    "    print(f\"\\n🚀 任务启动...\\n{'-'*40}\")\n",
    "    print(\"\\n🧠 [1/3] 军师正在制定初步策略...\")\n",
    "    search_strategy = profiler_agent.run(f\"请根据以下用户画像制定搜索策略：\\n\\n{profile_text}\")\n",
    "    print(f\"📝 策略: \\n{search_strategy}\")\n",
    "\n",
    "    # 2. 准备循环\n",
    "    keywords = [k.strip() for k in search_strategy.replace(\"，\", \",\").replace(\"\\n\", \",\").split(',') if k.strip()]\n",
    "    final_items = []\n",
    "    hunter = BatchSearchTool()\n",
    "\n",
    "    print(f\"\\n🔄 进入处理流程 (共 {len(keywords)} 个商品)...\")\n",
    "\n",
    "    for index, kw in enumerate(keywords):\n",
    "        print(f\"\\n    👉 [商品 {index+1}/{len(keywords)}] 正在处理: {kw}\")\n",
    "        \n",
    "        # 调用智能搜索函数 (传入 min 和 max)\n",
    "        valid_result, price_status, final_kw = find_best_product(hunter, profiler_agent, kw, b_min, b_max)\n",
    "        \n",
    "        if not valid_result:\n",
    "            print(\"       ❌ 彻底无数据，跳过。\")\n",
    "            continue\n",
    "            \n",
    "        # === 生成文案 ===\n",
    "        product_name = valid_result.get('title', final_kw)\n",
    "        \n",
    "        print(f\"       ✍️ 正在撰写文案: {product_name[:30]}...\")\n",
    "        pitch_prompt = f\"\"\"\n",
    "        商品：{product_name}\n",
    "        价格：{price_status}\n",
    "        卖点片段：{valid_result.get('content', '')[:200]}...\n",
    "        \n",
    "        请写一句30字以内的种草文案。\n",
    "        \"\"\"\n",
    "        pitch = pitcher_agent.run(pitch_prompt)\n",
    "        \n",
    "        final_items.append({\n",
    "            \"name\": final_kw, \n",
    "            \"title_full\": product_name,\n",
    "            \"price\": price_status,\n",
    "            \"desc\": pitch.replace(\"\\n\", \" \").strip(),\n",
    "            \"img\": valid_result.get('img', ''),\n",
    "            \"link\": valid_result.get('url', '')\n",
    "        })\n",
    "        print(f\"       ✅ 已收录 (状态: {price_status})\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0197ef43",
   "metadata": {},
   "source": [
    "\n",
    "### 第6部分：输出礼物计划"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8426fb13",
   "metadata": {},
   "outputs": [],
   "source": [
    "# --- 4. 渲染与保存 ---\n",
    "print(f\"\\n💾 正在生成最终报告...\")\n",
    "\n",
    "if not final_items:\n",
    "    final_md = \"很抱歉，网络搜索似乎出现了问题，未能获取到任何商品信息。\"\n",
    "else:\n",
    "    table_header = \"| 🎁 礼物名称 | 💰 价格 | ✨ 种草理由 | 🖼️ 图片/链接 |\\n| :--- | :--- | :--- | :--- |\\n\"\n",
    "    table_rows = []\n",
    "    \n",
    "    for item in final_items:\n",
    "        # 1. 清洗文本字段 (防止 | 破坏表格)\n",
    "        name = item.get('name', '未知').replace(\"|\", \"/\")\n",
    "        price = item.get('price', '暂无').replace(\"|\", \"/\")\n",
    "        desc = item.get('desc', '').replace(\"|\", \"/\")\n",
    "        \n",
    "        # 2. 🚨【核心修复】清洗链接中的竖线\n",
    "        # 百度/京东链接常包含 '|'，必须替换为 '%7C'，否则 Markdown 表格会炸\n",
    "        raw_link = item.get('link', '#')\n",
    "        safe_link = raw_link.replace(\"|\", \"%7C\")\n",
    "        \n",
    "        raw_img = item.get('img', '')\n",
    "        safe_img = raw_img.replace(\"|\", \"%7C\")\n",
    "        \n",
    "        # 3. 构建媒体列\n",
    "        if safe_img and safe_img.startswith(\"http\"):\n",
    "            # 图片链接套购买链接\n",
    "            media = f\"[![图]({safe_img})]({safe_link})\"\n",
    "        else:\n",
    "            media = f\"[点击购买]({safe_link})\"\n",
    "        \n",
    "        # 4. 组装行 (注意名字上的链接也要用 safe_link)\n",
    "        # 使用 strip() 去除可能的首尾空格\n",
    "        row = f\"| [{name}]({safe_link}) | {price} | {desc} | {media} |\"\n",
    "        table_rows.append(row)\n",
    "        \n",
    "    final_md = table_header + \"\\n\".join(table_rows)\n",
    "filename = \"outputs/gift_plan_output.md\"\n",
    "# 确保输出目录存在\n",
    "os.makedirs(os.path.dirname(filename), exist_ok=True)\n",
    "\n",
    "with open(filename, \"w\", encoding=\"utf-8\") as f:\n",
    "    f.write(final_md)\n",
    "print(f\"🎉 任务完成！文件已保存: {os.path.abspath(filename)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d8b884b5",
   "metadata": {},
   "source": [
    "### 第7部分：总结与展望"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "08b0b8e8",
   "metadata": {},
   "source": [
    "#### 实现的功能\n",
    "- 基于用户输入的个人信息，生成符合预算的礼物建议\n",
    "- 支持用户自定义预算范围、节日、个人喜好等\n",
    "- 支持百度MCP和Tavily API双数据源，利用搜索引擎获取最新的商品信息和价格\n",
    "- 提供可视化的建议结果展示\n",
    "#### 遇到的挑战与解决方案\n",
    "- 大模型的“幻觉”问题（JSON格式错误/编造数据）\n",
    "  - 解决方案：放弃让 LLM 直接生成最终数据。改为使用 Python 正则表达式 从搜索结果中暴力提取硬数据（价格、图片），仅让 LLM 负责生成文案。代码逻辑负责准确性，模型负责创造性。\n",
    "- 上下文过长导致提取失败\n",
    "  - 解决方案：结合实际业务场景，分析各个阶段对上下文的要求，在搜索阶段限制返回长度。同时通过拆分 “硬数据流”（找参数）和 “软数据流”（找卖点），大幅降低单次上下文长度，提升响应速度。\n",
    "- 大模型推荐的礼品价格超出预算\n",
    "  - 解决方案：引入检核机制。如果搜到的商品均价超预算，系统会自动呼叫“军师”重新制定“平替”策略，直到找到合适商品为止。\n",
    "- Agent传入的参数格式问题\n",
    "  - 解决方案：在工具层兼容 Agent 传入的各种参数格式（JSON/字符串、逗号/换行符分隔），确保搜索指令不丢失。\n",
    "#### 未来改进方向\n",
    "- 前端交互：开发前端页面，替代目前的Notebook交互，提供更好的用户交互体验\n",
    "- 数据源深度集成：完全接入百度优选MCP 的比价与历史价格接口，获取更精准的实时价格和库存信息，实现“全网比价”\n",
    "- 丰富选项：增加更多的个人喜好选项，如喜欢的商品类型、品牌等\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "ai_3.10",
   "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.10.19"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
