{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 课程 13 - 代理记忆\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 设置\n",
    "\n",
    "本笔记本演示了如何使用 **Microsoft Agent Framework** (MAF) 构建具有 <strong>持久记忆</strong> 的旅游预订代理。\n",
    "\n",
    "您将了解不同类型的代理记忆——工作记忆、短期记忆和长期记忆——如何影响代理在对话中的信息保留和使用。\n",
    "\n",
    "**先决条件：**\n",
    "- 一个部署了聊天模型（例如 `gpt-5-mini`）的 Microsoft Foundry 项目。\n",
    "- 已通过 Azure CLI 登录——在终端运行 `az login`。\n",
    "- `AZURE_AI_PROJECT_ENDPOINT` —— 您的 Microsoft Foundry 项目端点。\n",
    "- `AZURE_AI_MODEL_DEPLOYMENT_NAME` —— 您部署模型的名称。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%pip install agent-framework azure-ai-projects azure-identity python-dotenv -q"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import logging\n",
    "logging.getLogger(\"agent_framework.foundry\").setLevel(logging.ERROR)\n",
    "\n",
    "import os\n",
    "import json\n",
    "import dotenv\n",
    "from typing import Annotated\n",
    "from datetime import datetime\n",
    "\n",
    "from agent_framework import tool\n",
    "from agent_framework.foundry import FoundryChatClient\n",
    "from azure.identity import DefaultAzureCredential\n",
    "\n",
    "dotenv.load_dotenv()\n",
    "\n",
    "endpoint = os.getenv(\"AZURE_AI_PROJECT_ENDPOINT\")\n",
    "deployment_name = os.getenv(\"AZURE_AI_MODEL_DEPLOYMENT_NAME\")\n",
    "\n",
    "missing = [k for k, v in {\n",
    "    \"AZURE_AI_PROJECT_ENDPOINT\": endpoint,\n",
    "    \"AZURE_AI_MODEL_DEPLOYMENT_NAME\": deployment_name\n",
    "}.items() if not v]\n",
    "\n",
    "if missing:\n",
    "    raise ValueError(\n",
    "        f\"Missing required environment variables: {', '.join(missing)}. \"\n",
    "        \"Please set them as environment variables (e.g., in your .env file or shell environment).\"\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create the Microsoft Foundry client\n",
    "client = FoundryChatClient(\n",
    "    project_endpoint=endpoint,\n",
    "    model=deployment_name,\n",
    "    credential=DefaultAzureCredential()\n",
    ")\n",
    "\n",
    "print(\"Microsoft Foundry client configured\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 代理记忆的类型\n",
    "\n",
    "AI 代理可以利用不同类型的记忆，每种记忆都有其独特的用途：\n",
    "\n",
    "### 工作记忆\n",
    "会话线程本身——单次会话中交换的消息。代理可以回溯同一线程中的早期消息以保持连贯性。在 MAF 中，这是通过 **`agent.create_session()`** 创建的，它返回一个 `AgentSession`。\n",
    "\n",
    "### 短期记忆\n",
    "在任务或会话期间持续但不永久存储的信息。例如，代理可能在多轮规划对话中积累事实，并利用这些事实来生成最终的行程。\n",
    "\n",
    "### 长期记忆\n",
    "在<strong>跨会话</strong>中持续存在的偏好和事实。回访用户不应重复说明其饮食限制或旅行风格。长期记忆通常由外部存储支持——数据库、文件或向量索引——并通过工具向代理提供。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 使用会话的工作记忆\n",
    "\n",
    "最简单的记忆形式是对话会话。当你将同一个会话（通过 `agent.create_session()` 创建）传递给连续的 `agent.run()` 调用时，代理可以看到该对话的完整历史，并能够回忆起早期的细节。\n",
    "\n",
    "让我们创建一个旅行代理并演示工作记忆。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "agent = client.as_agent(\n",
    "    name=\"TravelMemoryAgent\",\n",
    "    instructions=(\n",
    "        \"You are a travel agent who remembers user preferences across conversations. \"\n",
    "        \"Track destinations mentioned, budget constraints, and travel dates.\"\n",
    "    ),\n",
    ")\n",
    "\n",
    "session = agent.create_session()\n",
    "\n",
    "# First message — the user shares preferences\n",
    "response = await agent.run(\n",
    "    \"I love beach destinations and my budget is $3000\",\n",
    "    session=session,\n",
    ")\n",
    "print(\"Agent:\", response)\n",
    "\n",
    "# Second message — the agent should recall the budget from the thread\n",
    "response = await agent.run(\n",
    "    \"What did I say my budget was?\",\n",
    "    session=session,\n",
    ")\n",
    "print(\"Agent:\", response)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "代理正确地回忆了预算，因为两个消息共享相同的会话。这是<strong>工作记忆</strong>——它只存在于会话的生命周期内。\n",
    "\n",
    "### 新线程会发生什么？\n",
    "\n",
    "如果我们创建一个<strong>新的</strong>会话，代理就不会记得之前的对话：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "new_session = agent.create_session()\n",
    "\n",
    "response = await agent.run(\n",
    "    \"What is my budget?\",\n",
    "    session=new_session,\n",
    ")\n",
    "print(\"Agent:\", response)\n",
    "print(\"\\n💡 The agent has no memory of the previous conversation — it's a fresh session.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 长期记忆模式\n",
    "\n",
    "为了记住用户偏好 <strong>跨会话</strong>，我们需要一个持久存储，它存在于对话线程之外。代理通过 <strong>工具</strong> 访问这个存储 —— 这些工具是它可以调用来保存和检索信息的函数。\n",
    "\n",
    "下面我们实现了一个简单的内存偏好存储（在生产中你会用数据库或向量索引来支持它），并将其作为代理可以使用的工具公开。\n",
    "\n",
    "### 架构\n",
    "```\n",
    "┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐\n",
    "│  MAF Agent      │────▶│  @tool functions  │────▶│  Preference     │\n",
    "│  (LLM)          │     │  save / retrieve  │     │  Store (dict)   │\n",
    "└─────────────────┘     └──────────────────┘     └─────────────────┘\n",
    "         │                                                 │\n",
    "    AgentSession                                   Persists across\n",
    "    (working memory)                               sessions\n",
    "```\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# --- Persistent preference store (simulated) ---\n",
    "preference_store: dict[str, list[str]] = {}\n",
    "\n",
    "\n",
    "@tool(approval_mode=\"never_require\")\n",
    "def save_preference(\n",
    "    user_id: Annotated[str, \"User identifier\"],\n",
    "    preference: Annotated[str, \"A travel preference to remember\"],\n",
    ") -> str:\n",
    "    \"\"\"Save a user travel preference to long-term memory.\"\"\"\n",
    "    preference_store.setdefault(user_id, []).append(preference)\n",
    "    return f\"✅ Stored: {preference}\"\n",
    "\n",
    "\n",
    "@tool(approval_mode=\"never_require\")\n",
    "def get_preferences(\n",
    "    user_id: Annotated[str, \"User identifier\"],\n",
    ") -> str:\n",
    "    \"\"\"Retrieve all saved travel preferences for a user.\"\"\"\n",
    "    prefs = preference_store.get(user_id, [])\n",
    "    if not prefs:\n",
    "        return f\"No saved preferences for {user_id}.\"\n",
    "    return \"Saved preferences:\\n- \" + \"\\n- \".join(prefs)\n",
    "\n",
    "\n",
    "@tool(approval_mode=\"never_require\")\n",
    "def search_hotels(\n",
    "    query: Annotated[str, \"Search query — location, amenities, or tags\"],\n",
    ") -> str:\n",
    "    \"\"\"Search the hotel database for matching properties.\"\"\"\n",
    "    hotels = [\n",
    "        {\"name\": \"Le Meurice Paris\", \"location\": \"Paris, France\", \"price\": 850, \"tags\": [\"luxury\", \"romantic\", \"spa\"]},\n",
    "        {\"name\": \"Four Seasons Maui\", \"location\": \"Maui, Hawaii\", \"price\": 695, \"tags\": [\"beach\", \"family\", \"resort\"]},\n",
    "        {\"name\": \"Aman Tokyo\", \"location\": \"Tokyo, Japan\", \"price\": 780, \"tags\": [\"luxury\", \"city\", \"spa\"]},\n",
    "        {\"name\": \"Hotel Sacher Vienna\", \"location\": \"Vienna, Austria\", \"price\": 420, \"tags\": [\"historic\", \"accessible\", \"cultural\"]},\n",
    "        {\"name\": \"Fairmont Whistler\", \"location\": \"Whistler, Canada\", \"price\": 380, \"tags\": [\"ski\", \"family\", \"mountain\"]},\n",
    "    ]\n",
    "    q = query.lower()\n",
    "    matches = [\n",
    "        h for h in hotels\n",
    "        if q in h[\"name\"].lower()\n",
    "        or q in h[\"location\"].lower()\n",
    "        or any(q in t for t in h[\"tags\"])\n",
    "    ]\n",
    "    if not matches:\n",
    "        matches = hotels[:3]\n",
    "    return json.dumps(matches, indent=2)\n",
    "\n",
    "\n",
    "print(\"✅ Tools defined: save_preference, get_preferences, search_hotels\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 场景1 — 第一次用户预订周年旅行\n",
    "\n",
    "Sarah 是首次访问。代理应通过工具存储她的偏好，并利用这些偏好推荐酒店。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "travel_agent = client.as_agent(\n",
    "    tools=[save_preference, get_preferences],\n",
    "    name=\"TravelBookingAssistant\",\n",
    "    instructions=(\n",
    "        \"You are a personalized travel booking assistant with long-term memory.\\n\"\n",
    "        \"WORKFLOW:\\n\"\n",
    "        \"1. When a user starts a conversation, call get_preferences() to check for saved information.\\n\"\n",
    "        \"2. Store any new preferences the user mentions using save_preference().\\n\"\n",
    "        \"3. Use search_hotels() to find suitable options that match their preferences and budget.\\n\"\n",
    "        \"4. Do NOT recommend hotels that exceed the user's budget.\\n\\n\"\n",
    "        \"IMPORTANT: Always use user_id='sarah_johnson_123' for all memory operations.\"\n",
    "    ),\n",
    ")\n",
    "\n",
    "session_1 = travel_agent.create_session()\n",
    "\n",
    "response = await travel_agent.run(\n",
    "    \"Hi! I'm Sarah and I'm planning a trip for my 10th wedding anniversary. \"\n",
    "    \"We love romantic destinations, fine dining, and spa experiences. \"\n",
    "    \"My husband has mobility issues, so we need accessible accommodations. \"\n",
    "    \"Our budget is around $700-800 per night.\",\n",
    "    session=session_1,\n",
    ")\n",
    "print(\"🤖 Agent:\", response)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "response = await travel_agent.run(\n",
    "    \"The Hotel Sacher sounds perfect! We're both vegetarian and I have a \"\n",
    "    \"severe nut allergy. Can you note that for future trips?\",\n",
    "    session=session_1,\n",
    ")\n",
    "print(\"🤖 Agent:\", response)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Verify what was stored\n",
    "print(\"📋 Preference store contents:\")\n",
    "for uid, prefs in preference_store.items():\n",
    "    print(f\"\\n  User: {uid}\")\n",
    "    for p in prefs:\n",
    "        print(f\"    - {p}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 场景 2 — Sarah 几周后回来\n",
    "\n",
    "Sarah 开始一个<strong>全新对话线程</strong>（模拟一个新会话）。工作内存是空的，但长期偏好存储中仍然有她的信息。代理应该检索这些信息并用它来个性化推荐。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "session_2 = travel_agent.create_session()  # New session — no working memory\n",
    "\n",
    "response = await travel_agent.run(\n",
    "    \"Hi, my husband and I are planning another trip. Can you recommend a good hotel?\",\n",
    "    session=session_2,\n",
    ")\n",
    "print(\"🤖 Agent:\", response)\n",
    "print(\"\\n💡 The agent retrieved Sarah's saved preferences from long-term memory \"\n",
    "      \"even though this is a completely new conversation thread.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "response = await travel_agent.run(\n",
    "    \"Great suggestions! For the Maui option, what activities would you recommend for the kids?\",\n",
    "    session=session_2,\n",
    ")\n",
    "print(\"🤖 Agent:\", response)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 总结\n",
    "\n",
    "在本课中，你了解了三种类型的代理记忆以及如何使用 Microsoft Agent Framework 实现它们：\n",
    "\n",
    "| 记忆类型 | MAF 机制 | 生命周期 |\n",
    "|---|---|---|\n",
    "| <strong>工作记忆</strong> | `agent.create_session()` | 单次对话 |\n",
    "| <strong>短期记忆</strong> | 线程内累积的上下文 | 单个任务 / 会话 |\n",
    "| <strong>长期记忆</strong> | 通过 `@tool` 函数访问的外部存储 | 跨会话 |\n",
    "\n",
    "### 关键要点\n",
    "1. **`agent.create_session()`** 提供工作记忆 — 代理在会话内能看到完整对话历史。\n",
    "2. <strong>新会话会丢失上下文</strong> — 如果没有长期记忆，代理无法回忆之前的对话。\n",
    "3. **`@tool` 函数搭建桥梁** — 允许代理保存和检索持久存储中的信息。\n",
    "4. <strong>个性化随着时间提升</strong> — 存储的偏好越多，代理的推荐越精准。\n",
    "\n",
    "### 现实应用\n",
    "- <strong>客户服务</strong>：记住客户历史和偏好\n",
    "- <strong>个人助理</strong>：跨天或数周保持上下文\n",
    "- <strong>医疗保健</strong>：跟踪患者信息和偏好\n",
    "- <strong>电子商务</strong>：基于历史提供个性化购物\n",
    "\n",
    "### 后续步骤\n",
    "- 用数据库或向量存储（如 Azure AI 搜索）替换内存中的字典\n",
    "- 为时效性信息添加记忆过期机制\n",
    "- 构建具有共享记忆的多代理系统\n",
    "- 探索 Cognee 笔记本以实现基于知识图谱的记忆\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n\n<!-- CO-OP TRANSLATOR DISCLAIMER START -->\n**免责声明**：\n本文件由 AI 翻译服务 [Co-op Translator](https://github.com/Azure/co-op-translator) 翻译完成。尽管我们力求准确，但请注意，自动翻译可能包含错误或不准确之处。原始语言版文件应视为权威来源。对于重要信息，建议使用专业人工翻译。我们对因使用本翻译而产生的任何误解或误释不承担责任。\n<!-- CO-OP TRANSLATOR DISCLAIMER END -->\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "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.12.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}