{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "48da9854",
   "metadata": {},
   "source": [
    "# 第16课 - 使用 Microsoft Foundry 部署可扩展代理\n",
    "\n",
    "在本笔记本中，您将构建一个适用于虚构公司 **Contoso** 的<strong>生产就绪客户支持代理</strong>。与之前的课程不同，重点不是代理的推理循环——而是围绕它构建的所有内容，使代理能够安全地大规模运行：\n",
    "\n",
    "1. <strong>工具调用</strong> — 订单查询和工单创建。\n",
    "2. **RAG** — 来自知识库的策略答案。\n",
    "3. <strong>记忆</strong> — 跨回合记住客户信息。\n",
    "4. <strong>模型路由</strong> — 简单请求发送到小模型，复杂请求发送到大模型。\n",
    "5. <strong>响应缓存</strong> — 对重复问题无需模型调用直接响应。\n",
    "6. <strong>人工审批</strong> — 超过阈值的退款需暂停以等待批准。\n",
    "7. <strong>评估门控</strong> — 阻止错误发布的离线测试集。\n",
    "8. <strong>可观测性</strong> — 每个请求的 OpenTelemetry 跟踪。\n",
    "\n",
    "每个章节都是独立且可运行的。请逐行阅读——生产原语保持故意精简。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cbba23dd",
   "metadata": {},
   "source": [
    "## 设置\n",
    "\n",
    "在运行此笔记本之前，请确保您已经：\n",
    "\n",
    "1. **拥有一个已部署聊天模型的 Microsoft Foundry 项目**（例如 `gpt-5-mini`）。\n",
    "2. **已使用 Azure CLI 登录** — 在终端中运行 `az login`。\n",
    "3. **设置了所需的环境变量：**\n",
    "   - `AZURE_AI_PROJECT_ENDPOINT` — 您的 Microsoft Foundry 项目端点。\n",
    "   - `AZURE_AI_MODEL_DEPLOYMENT_NAME` — 您已部署模型的名称。\n",
    "\n",
    "当设置了 `AZURE_SEARCH_SERVICE_ENDPOINT` 和 `AZURE_SEARCH_API_KEY` 时，RAG 部分使用 **Azure AI Search**，否则回退到内存搜索，以确保笔记本在无 Search 资源情况下也能运行。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "585794b8",
   "metadata": {},
   "outputs": [],
   "source": [
    "%pip install agent-framework azure-ai-projects azure-identity python-dotenv -q"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d78d98ef",
   "metadata": {},
   "outputs": [],
   "source": [
    "import logging\n",
    "logging.getLogger(\"agent_framework.foundry\").setLevel(logging.ERROR)\n",
    "\n",
    "import os\n",
    "import re\n",
    "import dotenv\n",
    "from typing import Annotated\n",
    "\n",
    "from agent_framework import tool\n",
    "from agent_framework.foundry import FoundryChatClient\n",
    "from azure.identity import AzureCliCredential\n",
    "\n",
    "dotenv.load_dotenv(dotenv.find_dotenv())\n",
    "\n",
    "endpoint = os.getenv(\"AZURE_AI_PROJECT_ENDPOINT\")\n",
    "model = os.getenv(\"AZURE_AI_MODEL_DEPLOYMENT_NAME\")\n",
    "\n",
    "if not endpoint or not model:\n",
    "    raise ValueError(\n",
    "        \"Missing required environment variables. \"\n",
    "        \"Please set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME in your .env file.\"\n",
    "    )\n",
    "\n",
    "provider = FoundryChatClient(\n",
    "    project_endpoint=endpoint,\n",
    "    model=model,\n",
    "    credential=AzureCliCredential(),\n",
    ")\n",
    "print(\"Foundry client ready.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b1e4f8a3",
   "metadata": {},
   "source": [
    "## 1. 工具\n",
    "\n",
    "生产工具针对真实系统执行实际工作。这里我们用纯 Python 函数模拟一个订单数据库和票务系统。`@tool` 装饰器将它们暴露给代理。\n",
    "\n",
    "注意 `issue_refund` 对超出阈值的退款使用了 `approval_mode=\"always_require\"` —— 这是我们后续部署的人机交互原语。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "41a10845",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Simulated backend systems (in production these are API calls behind scoped identities).\n",
    "ORDERS = {\n",
    "    \"A1001\": {\"status\": \"shipped\", \"total\": 42.00, \"eta\": \"2 days\"},\n",
    "    \"A1002\": {\"status\": \"processing\", \"total\": 128.50, \"eta\": \"5 days\"},\n",
    "    \"A1003\": {\"status\": \"delivered\", \"total\": 19.99, \"eta\": \"delivered\"},\n",
    "}\n",
    "TICKETS: list[dict] = []\n",
    "REFUND_APPROVAL_THRESHOLD = 50.0\n",
    "\n",
    "\n",
    "@tool(approval_mode=\"never_require\")\n",
    "def get_order_status(order_id: Annotated[str, \"The customer's order ID, e.g. A1001\"]) -> str:\n",
    "    \"\"\"Look up the status of a customer order.\"\"\"\n",
    "    order = ORDERS.get(order_id.upper())\n",
    "    if not order:\n",
    "        return f\"No order found with ID {order_id}.\"\n",
    "    return (\n",
    "        f\"Order {order_id.upper()}: status={order['status']}, \"\n",
    "        f\"total=${order['total']:.2f}, eta={order['eta']}.\"\n",
    "    )\n",
    "\n",
    "\n",
    "@tool(approval_mode=\"never_require\")\n",
    "def open_ticket(\n",
    "    subject: Annotated[str, \"Short subject line for the support ticket\"],\n",
    "    details: Annotated[str, \"Full description of the customer's issue\"],\n",
    ") -> str:\n",
    "    \"\"\"Open a support ticket for issues that need human follow-up.\"\"\"\n",
    "    ticket_id = f\"T{1000 + len(TICKETS) + 1}\"\n",
    "    TICKETS.append({\"id\": ticket_id, \"subject\": subject, \"details\": details})\n",
    "    return f\"Ticket {ticket_id} opened: {subject}\"\n",
    "\n",
    "\n",
    "def refund_needs_approval(amount: float) -> bool:\n",
    "    \"\"\"Refunds above the threshold require a human approver.\"\"\"\n",
    "    return amount > REFUND_APPROVAL_THRESHOLD\n",
    "\n",
    "\n",
    "@tool(approval_mode=\"always_require\")\n",
    "def issue_refund(\n",
    "    order_id: Annotated[str, \"The order to refund\"],\n",
    "    amount: Annotated[float, \"Refund amount in USD\"],\n",
    ") -> str:\n",
    "    \"\"\"Issue a refund. Execution pauses for human approval before it runs.\"\"\"\n",
    "    return f\"Refund of ${amount:.2f} issued for order {order_id.upper()}.\"\n",
    "\n",
    "\n",
    "print(\"Tools defined.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5a1d9437",
   "metadata": {},
   "source": [
    "## 2. RAG — 政策知识库\n",
    "\n",
    "政策问题（“您的退货期限是多少？”）应从权威来源获得答案，而不是依赖模型的记忆。我们将一个小型知识库封装为搜索工具。\n",
    "\n",
    "在生产环境中，这是 **Azure AI Search**；在这里我们提供了一个内存中的关键词搜索，以便笔记本可以在任何地方运行，当环境变量存在时会自动切换到 Azure AI Search。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "acf21012",
   "metadata": {},
   "outputs": [],
   "source": [
    "KNOWLEDGE_BASE = {\n",
    "    \"returns\": \"Contoso accepts returns within 30 days of delivery for a full refund. Items must be unused and in original packaging.\",\n",
    "    \"shipping\": \"Standard shipping takes 3-5 business days. Express shipping (1-2 days) is available at checkout for an extra fee.\",\n",
    "    \"warranty\": \"All Contoso electronics carry a 12-month limited warranty covering manufacturing defects.\",\n",
    "    \"refund_policy\": \"Refunds are processed to the original payment method within 5 business days of approval. Refunds over $50 require a supervisor's approval.\",\n",
    "}\n",
    "\n",
    "\n",
    "def _in_memory_search(query: str) -> str:\n",
    "    q = query.lower()\n",
    "    hits = [text for key, text in KNOWLEDGE_BASE.items() if key.replace(\"_\", \" \") in q or key in q]\n",
    "    if not hits:\n",
    "        # crude keyword fallback so the tool still returns something useful\n",
    "        hits = [text for text in KNOWLEDGE_BASE.values() if any(w in text.lower() for w in q.split())]\n",
    "    return \"\\n\".join(hits) if hits else \"No matching policy found.\"\n",
    "\n",
    "\n",
    "def _azure_search(query: str) -> str:\n",
    "    from azure.core.credentials import AzureKeyCredential\n",
    "    from azure.search.documents import SearchClient\n",
    "\n",
    "    client = SearchClient(\n",
    "        endpoint=os.environ[\"AZURE_SEARCH_SERVICE_ENDPOINT\"],\n",
    "        index_name=os.getenv(\"AZURE_SEARCH_INDEX_NAME\", \"contoso-policies\"),\n",
    "        credential=AzureKeyCredential(os.environ[\"AZURE_SEARCH_API_KEY\"]),\n",
    "    )\n",
    "    results = client.search(search_text=query, top=3)\n",
    "    return \"\\n\".join(r.get(\"content\", \"\") for r in results) or \"No matching policy found.\"\n",
    "\n",
    "\n",
    "USE_AZURE_SEARCH = bool(os.getenv(\"AZURE_SEARCH_SERVICE_ENDPOINT\") and os.getenv(\"AZURE_SEARCH_API_KEY\"))\n",
    "\n",
    "\n",
    "@tool(approval_mode=\"never_require\")\n",
    "def search_policies(query: Annotated[str, \"The policy question to look up\"]) -> str:\n",
    "    \"\"\"Search Contoso support policies to answer customer questions.\"\"\"\n",
    "    if USE_AZURE_SEARCH:\n",
    "        return _azure_search(query)\n",
    "    return _in_memory_search(query)\n",
    "\n",
    "\n",
    "print(f\"RAG ready. Using {'Azure AI Search' if USE_AZURE_SEARCH else 'in-memory search'}.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a58923d2",
   "metadata": {},
   "source": [
    "## 3. 内存\n",
    "\n",
    "一个忘记自己在和谁交谈的支持代理是一个糟糕的支持代理。我们为每个客户保留一个微小的个人资料存储，并将一个简短的摘要注入代理的指令中。在生产环境中，这是一项内存服务（见第13课）；这里用字典使该模式可见。 \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "65e61110",
   "metadata": {},
   "outputs": [],
   "source": [
    "CUSTOMER_MEMORY: dict[str, dict] = {\n",
    "    \"cust-42\": {\"name\": \"Dana\", \"tier\": \"enterprise\", \"recent_order\": \"A1002\"},\n",
    "    \"cust-99\": {\"name\": \"Sam\", \"tier\": \"standard\", \"recent_order\": \"A1003\"},\n",
    "}\n",
    "\n",
    "\n",
    "def memory_context(customer_id: str) -> str:\n",
    "    profile = CUSTOMER_MEMORY.get(customer_id)\n",
    "    if not profile:\n",
    "        return \"This is a new customer with no history.\"\n",
    "    return (\n",
    "        f\"Customer {profile['name']} ({profile['tier']} tier). \"\n",
    "        f\"Most recent order: {profile['recent_order']}.\"\n",
    "    )\n",
    "\n",
    "\n",
    "print(memory_context(\"cust-42\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b5c318bf",
   "metadata": {},
   "source": [
    "## 4 & 5. 模型路由和响应缓存\n",
    "\n",
    "两个成本杠杆连接到单个请求处理器：\n",
    "\n",
    "- <strong>路由</strong>：一个廉价的启发式分类器决定请求是需要小模型还是大模型。\n",
    "- <strong>缓存</strong>：标准化的重复问题直接从缓存提供，无需调用模型。\n",
    "\n",
    "这里的分类器故意设计得很简单。在生产环境中，你会针对流量验证它，并且可以用 Foundry 的模型路由器替代它。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d8456988",
   "metadata": {},
   "outputs": [],
   "source": [
    "SMALL_MODEL = os.getenv(\"AZURE_AI_SMALL_MODEL\", model)   # e.g. gpt-5-nano\n",
    "LARGE_MODEL = os.getenv(\"AZURE_AI_LARGE_MODEL\", model)   # e.g. gpt-5-mini\n",
    "\n",
    "response_cache: dict[str, str] = {}\n",
    "route_counters = {\"small\": 0, \"large\": 0, \"cache\": 0}\n",
    "\n",
    "\n",
    "def normalize(query: str) -> str:\n",
    "    return re.sub(r\"\\s+\", \" \", query.lower().strip())\n",
    "\n",
    "\n",
    "COMPLEX_SIGNALS = (\"refund\", \"cancel\", \"complaint\", \"escalate\", \"broken\", \"wrong\", \"why\")\n",
    "\n",
    "\n",
    "def is_simple(query: str) -> bool:\n",
    "    \"\"\"Route complex or high-stakes requests to the large model; everything else to the small one.\"\"\"\n",
    "    q = query.lower()\n",
    "    if any(signal in q for signal in COMPLEX_SIGNALS):\n",
    "        return False\n",
    "    return len(q.split()) <= 20\n",
    "\n",
    "\n",
    "def choose_model(query: str) -> str:\n",
    "    return SMALL_MODEL if is_simple(query) else LARGE_MODEL\n",
    "\n",
    "\n",
    "print(f\"Small model: {SMALL_MODEL} | Large model: {LARGE_MODEL}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "87fa7e7f",
   "metadata": {},
   "source": [
    "## 6 & 8. 代理、人类审批和可观测性\n",
    "\n",
    "现在我们从上述工具组装代理，并将每个请求包装在 OpenTelemetry span 中。`handle_support_request` 函数是生产请求处理器：缓存 → 路由 → 跟踪 → 运行 → 缓存。\n",
    "\n",
    "人类审批由框架处理：因为 `issue_refund` 的 `approval_mode=\"always_require\"`，运行会暂停并呈现一个审批请求，等待我们明确解决。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "01c62cfc",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Tracing: use the Agent Framework tracer if available, else a no-op so the notebook runs anywhere.\n",
    "try:\n",
    "    from agent_framework.observability import get_tracer\n",
    "    tracer = get_tracer()\n",
    "except Exception:  # observability extras not installed\n",
    "    from contextlib import contextmanager\n",
    "\n",
    "    class _NoopSpan:\n",
    "        def set_attribute(self, *_args, **_kwargs):\n",
    "            pass\n",
    "\n",
    "    class _NoopTracer:\n",
    "        @contextmanager\n",
    "        def start_as_current_span(self, _name):\n",
    "            yield _NoopSpan()\n",
    "\n",
    "    tracer = _NoopTracer()\n",
    "\n",
    "\n",
    "SUPPORT_INSTRUCTIONS = (\n",
    "    \"You are Contoso's customer support agent. Be concise, friendly, and accurate. \"\n",
    "    \"Use search_policies for policy questions, get_order_status for orders, \"\n",
    "    \"open_ticket when a human needs to follow up, and issue_refund for refunds. \"\n",
    "    \"Never invent policy details.\"\n",
    ")\n",
    "\n",
    "# Build one agent per model tier so we can route by cost. The current agent-framework\n",
    "# selects the model on the client, so each tier gets its own FoundryChatClient.\n",
    "_TOOLS = [get_order_status, open_ticket, search_policies, issue_refund]\n",
    "_agents_by_model: dict[str, object] = {}\n",
    "\n",
    "\n",
    "def agent_for(model_name: str):\n",
    "    if model_name not in _agents_by_model:\n",
    "        client = FoundryChatClient(\n",
    "            project_endpoint=endpoint,\n",
    "            model=model_name,\n",
    "            credential=AzureCliCredential(),\n",
    "        )\n",
    "        _agents_by_model[model_name] = client.as_agent(\n",
    "            name=\"ContosoSupportAgent\",\n",
    "            instructions=SUPPORT_INSTRUCTIONS,\n",
    "            tools=_TOOLS,\n",
    "        )\n",
    "    return _agents_by_model[model_name]\n",
    "\n",
    "\n",
    "# Default agent (used by the evaluation gate, which does not route).\n",
    "support_agent = agent_for(SMALL_MODEL)\n",
    "\n",
    "\n",
    "async def handle_support_request(query: str, customer_id: str) -> str:\n",
    "    # 1. Serve from cache when we can.\n",
    "    key = normalize(query)\n",
    "    if key in response_cache:\n",
    "        route_counters[\"cache\"] += 1\n",
    "        return response_cache[key]\n",
    "\n",
    "    # 2. Route by complexity to control cost.\n",
    "    chosen_model = choose_model(query)\n",
    "    route_counters[\"small\" if chosen_model == SMALL_MODEL else \"large\"] += 1\n",
    "\n",
    "    # 3. Add per-customer memory to the prompt.\n",
    "    context = memory_context(customer_id)\n",
    "    prompt = f\"[Customer context: {context}]\\n\\n{query}\"\n",
    "\n",
    "    # 4. Run inside a trace span for observability.\n",
    "    with tracer.start_as_current_span(\"support_request\") as span:\n",
    "        span.set_attribute(\"customer.id\", customer_id)\n",
    "        span.set_attribute(\"routed.model\", chosen_model)\n",
    "        response = await agent_for(chosen_model).run(prompt)\n",
    "\n",
    "    text = response.text\n",
    "    response_cache[key] = text\n",
    "    return text\n",
    "\n",
    "\n",
    "print(\"Support agent assembled.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "478703e5",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Try a few requests. The first is simple (small model), the second is a refund (large model + approval path).\n",
    "print(await handle_support_request(\"What is your return window?\", \"cust-99\"))\n",
    "print(\"---\")\n",
    "print(await handle_support_request(\"Where is my order A1002?\", \"cust-42\"))\n",
    "print(\"---\")\n",
    "# Repeat the first question -> served from cache.\n",
    "print(await handle_support_request(\"What is your return window?\", \"cust-99\"))\n",
    "print(\"---\")\n",
    "print(\"Routing counters:\", route_counters)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3bae744b",
   "metadata": {},
   "source": [
    "## 7. 评估门\n",
    "\n",
    "这是课程中的发布门：一个离线测试集对代理进行评分，只有通过率超过阈值后才进行部署。这里的评分器是一个简单的关键词重叠检查，以保持笔记本的自包含；在生产环境中，你会使用作为裁判的LLM或框架评估器（参见第10课）。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "30b21256",
   "metadata": {},
   "outputs": [],
   "source": [
    "TEST_CASES = [\n",
    "    {\"input\": \"How long do I have to return an item?\", \"expected\": [\"30 days\", \"refund\"]},\n",
    "    {\"input\": \"How fast is standard shipping?\", \"expected\": [\"3-5\", \"business days\"]},\n",
    "    {\"input\": \"What is the status of order A1001?\", \"expected\": [\"shipped\", \"A1001\"]},\n",
    "    {\"input\": \"Do your electronics have a warranty?\", \"expected\": [\"12-month\", \"warranty\"]},\n",
    "]\n",
    "\n",
    "\n",
    "def score_response(actual: str, expected_keywords: list[str]) -> float:\n",
    "    actual_l = actual.lower()\n",
    "    hits = sum(1 for kw in expected_keywords if kw.lower() in actual_l)\n",
    "    return hits / len(expected_keywords)\n",
    "\n",
    "\n",
    "async def evaluation_gate(test_cases: list[dict], threshold: float = 0.8) -> bool:\n",
    "    passed = 0\n",
    "    for case in test_cases:\n",
    "        result = await support_agent.run(case[\"input\"])\n",
    "        s = score_response(result.text, case[\"expected\"])\n",
    "        status = \"PASS\" if s >= 0.5 else \"FAIL\"\n",
    "        print(f\"[{status}] {case['input']}  (score={s:.0%})\")\n",
    "        if s >= 0.5:\n",
    "            passed += 1\n",
    "    pass_rate = passed / len(test_cases)\n",
    "    print(f\"\\nEvaluation pass rate: {pass_rate:.0%} (gate: {threshold:.0%})\")\n",
    "    return pass_rate >= threshold\n",
    "\n",
    "\n",
    "gate_passed = await evaluation_gate(TEST_CASES, threshold=0.8)\n",
    "print(\"\\nDeploy allowed:\" , gate_passed)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4cb0c705",
   "metadata": {},
   "source": [
    "## 综合应用：模拟发布\n",
    "\n",
    "下面的单元展示了课程描述的整个循环：运行评估门，只有通过时才“部署”。这是在将代理版本发布到 Foundry Agent Service 之前，在持续集成中运行的模式。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "be311368",
   "metadata": {},
   "outputs": [],
   "source": [
    "async def release(test_cases: list[dict]) -> None:\n",
    "    print(\"Running pre-deployment evaluation gate...\\n\")\n",
    "    if await evaluation_gate(test_cases, threshold=0.8):\n",
    "        print(\"\\n✅ Gate passed — promoting agent version to the Foundry Agent Service.\")\n",
    "    else:\n",
    "        print(\"\\n❌ Gate failed — release blocked. Fix the agent and re-run.\")\n",
    "\n",
    "\n",
    "await release(TEST_CASES)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6c1333c0",
   "metadata": {},
   "source": [
    "## 总结\n",
    "\n",
    "你组装了一个准备好投入生产的客户支持代理，所有运营问题均已接入：\n",
    "\n",
    "- **工具、RAG 和记忆** 赋予代理能力和上下文。\n",
    "- <strong>模型路由和缓存</strong> 控制延迟和成本。\n",
    "- <strong>人工审批</strong> 保护高风险操作，如大额退款。\n",
    "- <strong>评估关卡</strong> 阻止不良版本发布。\n",
    "- <strong>追踪</strong> 使每个请求可观察。\n",
    "\n",
    "### 挑战\n",
    "\n",
    "扩展此代理以：\n",
    "\n",
    "1. <strong>支持多模型</strong> — 添加第三个“推理”层并将升级/投诉路由到该层。\n",
    "2. <strong>添加评估关卡</strong> — 扩展 `TEST_CASES` 以包含退款审批场景，并确认关卡能捕获回归。\n",
    "3. <strong>添加成本感知路由</strong> — 跟踪每个请求的估计成本（小额 vs 大额 vs 缓存），并在一批混合查询后打印成本报告。\n",
    "\n",
    "在下一课中，你将走相反的路线，使用 Microsoft Foundry Local 和 Qwen 完全在自己的机器上运行代理。\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": {
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}