{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 介绍 \n",
    "\n",
    "本课将涵盖： \n",
    "- 什么是函数调用及其使用场景 \n",
    "- 如何使用 Azure OpenAI 创建函数调用 \n",
    "- 如何将函数调用集成到应用程序中 \n",
    "\n",
    "## 学习目标 \n",
    "\n",
    "完成本课后，您将了解并掌握： \n",
    "\n",
    "- 使用函数调用的目的 \n",
    "- 使用 Azure Open AI 服务设置函数调用 \n",
    "- 针对您的应用场景设计有效的函数调用 \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 理解函数调用 \n",
    "\n",
    "在本课中，我们希望为我们的教育初创公司构建一个功能，允许用户使用聊天机器人查找技术课程。我们将推荐符合他们技能水平、当前角色和感兴趣技术的课程。 \n",
    "\n",
    "为完成此任务，我们将结合使用： \n",
    " - `Azure Open AI` 为用户创建聊天体验\n",
    " - `Microsoft Learn Catalog API` 根据用户请求帮助用户查找课程 \n",
    " - `Function Calling` 接收用户查询并调用函数以发起 API 请求。 \n",
    "\n",
    "首先，让我们看看为什么我们首先想要使用函数调用： \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 为什么要调用函数\n",
    "\n",
    "如果你已经完成了本课程中的其他任何课程，你可能已经理解了使用大型语言模型（LLM）的强大功能。希望你也能看到它们的一些局限性。\n",
    "\n",
    "调用函数是 Azure Open AI 服务的一个功能，用以克服以下限制：\n",
    "1) 响应格式一致性\n",
    "2) 能够在聊天上下文中使用应用程序其他来源的数据\n",
    "\n",
    "在调用函数之前，LLM 的响应是非结构化且不一致的。开发者需要编写复杂的验证代码，以确保能够处理响应的每种变体。\n",
    "\n",
    "用户无法得到诸如“斯德哥尔摩当前天气如何？”这样的答案。这是因为模型的知识限于训练数据的时间点。\n",
    "\n",
    "让我们看下面的示例来说明这个问题：\n",
    "\n",
    "假设我们想创建一个学生数据数据库，以便为他们推荐合适的课程。下面有两个学生的描述，它们所含数据非常相似。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "student_1_description=\"Emily Johnson is a sophomore majoring in computer science at Duke University. She has a 3.7 GPA. Emily is an active member of the university's Chess Club and Debate Team. She hopes to pursue a career in software engineering after graduating.\"\n",
    " \n",
    "student_2_description = \"Michael Lee is a sophomore majoring in computer science at Stanford University. He has a 3.8 GPA. Michael is known for his programming skills and is an active member of the university's Robotics Club. He hopes to pursue a career in artificial intelligence after finishing his studies.\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "我们想把这个发送给大语言模型来解析数据。稍后可以在我们的应用程序中使用它，将数据发送到 API 或存储在数据库中。\n",
    "\n",
    "让我们创建两个相同的提示，告诉大语言模型我们感兴趣的信息：\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们想把这个发送给一个大型语言模型（LLM），让它解析对我们的产品重要的部分。这样我们就可以创建两个相同的提示来指导LLM：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prompt1 = f'''\n",
    "Please extract the following information from the given text and return it as a JSON object:\n",
    "\n",
    "name\n",
    "major\n",
    "school\n",
    "grades\n",
    "club\n",
    "\n",
    "This is the body of text to extract the information from:\n",
    "{student_1_description}\n",
    "'''\n",
    "\n",
    "\n",
    "prompt2 = f'''\n",
    "Please extract the following information from the given text and return it as a JSON object:\n",
    "\n",
    "name\n",
    "major\n",
    "school\n",
    "grades\n",
    "club\n",
    "\n",
    "This is the body of text to extract the information from:\n",
    "{student_2_description}\n",
    "'''\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "创建这两个提示后，我们将使用 `client.responses.create` 将它们发送给大语言模型。我们将提示存储在 `input` 变量中，并将角色分配为 `user`。这样做是为了模拟用户向聊天机器人发送消息。 \n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "ename": "",
     "evalue": "",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31mRunning cells with '/opt/homebrew/bin/python3' requires the ipykernel package.\n",
      "\u001b[1;31mRun the following command to install 'ipykernel' into the Python environment. \n",
      "\u001b[1;31mCommand: '/opt/homebrew/bin/python3 -m pip install ipykernel -U --user --force-reinstall'"
     ]
    }
   ],
   "source": [
    "import os\n",
    "import json\n",
    "from openai import OpenAI\n",
    "from dotenv import load_dotenv\n",
    "load_dotenv()\n",
    "\n",
    "# The OpenAI client points at the Azure OpenAI (Microsoft Foundry) /openai/v1/ endpoint\n",
    "client = OpenAI(\n",
    "  api_key=os.environ['AZURE_OPENAI_API_KEY'],\n",
    "  base_url=f\"{os.environ['AZURE_OPENAI_ENDPOINT'].rstrip('/')}/openai/v1/\",\n",
    "  )\n",
    "\n",
    "deployment=os.environ['AZURE_OPENAI_DEPLOYMENT']\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在我们可以向 LLM 发送这两个请求，并检查我们收到的响应。 \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "openai_response1 = client.responses.create(\n",
    " model=deployment,    \n",
    " input = [{'role': 'user', 'content': prompt1}],\n",
    " store=False,\n",
    ")\n",
    "openai_response1.output_text \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "openai_response2 = client.responses.create(\n",
    " model=deployment,    \n",
    " input = [{'role': 'user', 'content': prompt2}],\n",
    " store=False,\n",
    ")\n",
    "openai_response2.output_text\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Loading the response as a JSON object\n",
    "json_response1 = json.loads(openai_response1.output_text)\n",
    "json_response1\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Loading the response as a JSON object\n",
    "json_response2 = json.loads(openai_response2.output_text)\n",
    "json_response2\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "即使提示相同且描述类似，我们也可能得到不同格式的 `Grades` 属性。\n",
    "\n",
    "如果多次运行上述单元格，格式可能是 `3.7` 或 `3.7 GPA`。\n",
    "\n",
    "这是因为大语言模型（LLM）接收的是以书面提示形式存在的非结构化数据，返回的也同样是非结构化数据。我们需要有一个结构化的格式，以便在存储或使用这些数据时知道该期待什么。\n",
    "\n",
    "通过使用函数调用，我们可以确保收到结构化的数据。使用函数调用时，LLM 并不实际调用或运行任何函数。相反，我们为 LLM 创建一个结构让它遵循，以便其响应。然后我们使用这些结构化的响应来知道在应用中该运行哪个函数。\n",
    " \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![函数调用流程图](../../../../translated_images/zh-CN/Function-Flow.083875364af4f4bb.webp)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "然后我们可以将函数返回的内容发送回LLM。LLM随后将使用自然语言回应，以回答用户的查询。 \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 使用函数调用的用例 \n",
    "\n",
    "<strong>调用外部工具</strong> \n",
    "聊天机器人非常擅长为用户提供问题答案。通过使用函数调用，聊天机器人可以利用用户的消息来完成某些任务。例如，学生可以请求聊天机器人“发送电子邮件给我的导师，说我需要更多关于这门课程的帮助”。这时可以调用函数 `send_email(to: string, body: string)`。\n",
    "\n",
    "\n",
    "**创建 API 或数据库查询**\n",
    "用户可以通过自然语言查找信息，并将其转换为格式化的查询或 API 请求。比如，一位老师请求“谁完成了最后一次作业”，这可以调用一个名为 `get_completed(student_name: string, assignment: int, current_status: string)` 的函数。\n",
    "\n",
    "\n",
    "<strong>创建结构化数据</strong>\n",
    "用户可以使用 LLM 从一段文本或 CSV 中提取重要信息。例如，学生可以将一篇关于和平协议的维基百科文章转换成 AI 闪卡。这个过程可以通过调用函数 `get_important_facts(agreement_name: string, date_signed: string, parties_involved: list)` 来完成。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. 创建你的第一个函数调用\n",
    "\n",
    "创建函数调用的过程包括三个主要步骤：\n",
    "1. 使用你的函数列表和用户消息调用聊天完成 API\n",
    "2. 阅读模型的响应以执行操作，即执行函数或 API 调用\n",
    "3. 使用函数的响应再次调用聊天完成 API，利用该信息为用户生成响应。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![函数调用流程](../../../../translated_images/zh-CN/LLM-Flow.3285ed8caf4796d7.webp)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 函数调用的元素 \n",
    "\n",
    "#### 用户输入 \n",
    "\n",
    "第一步是创建一个用户消息。这可以通过获取文本输入的值动态分配，或者你也可以在这里分配一个值。如果这是你第一次使用聊天补全 API，我们需要定义消息的 `role` 和 `content`。\n",
    "\n",
    "`role` 可以是 `system`（创建规则）、`assistant`（模型）或 `user`（最终用户）。对于函数调用，我们将其分配为 `user` 并给出一个示例问题。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "messages= [ {\"role\": \"user\", \"content\": \"Find me a good course for a beginner student to learn Azure.\"} ]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 创建函数。\n",
    "\n",
    "接下来我们将定义一个函数以及该函数的参数。这里我们只使用一个名为 `search_courses` 的函数，但你可以创建多个函数。\n",
    "\n",
    "<strong>重要</strong> ：函数包含在发送给大型语言模型的系统消息中，并且会计入你可用的令牌数量。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# The Responses API uses a flat tool format: name/description/parameters at the top level\n",
    "functions = [\n",
    "   {\n",
    "      \"type\":\"function\",\n",
    "      \"name\":\"search_courses\",\n",
    "      \"description\":\"Retrieves courses from the search index based on the parameters provided\",\n",
    "      \"parameters\":{\n",
    "         \"type\":\"object\",\n",
    "         \"properties\":{\n",
    "            \"role\":{\n",
    "               \"type\":\"string\",\n",
    "               \"description\":\"The role of the learner (i.e. developer, data scientist, student, etc.)\"\n",
    "            },\n",
    "            \"product\":{\n",
    "               \"type\":\"string\",\n",
    "               \"description\":\"The product that the lesson is covering (i.e. Azure, Power BI, etc.)\"\n",
    "            },\n",
    "            \"level\":{\n",
    "               \"type\":\"string\",\n",
    "               \"description\":\"The level of experience the learner has prior to taking the course (i.e. beginner, intermediate, advanced)\"\n",
    "            }\n",
    "         },\n",
    "         \"required\":[\n",
    "            \"role\"\n",
    "         ]\n",
    "      }\n",
    "   }\n",
    "]\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<strong>定义</strong> \n",
    "\n",
    "`name` - 我们希望调用的函数名称。 \n",
    "\n",
    "`description` - 这是函数如何工作的描述。这里重要的是具体且清晰 \n",
    "\n",
    "`parameters` - 您希望模型在响应中生成的值和格式列表 \n",
    "\n",
    "\n",
    "`type` -  属性将存储的数据类型 \n",
    "\n",
    "`properties` - 模型将用于响应的具体值列表 \n",
    "\n",
    "\n",
    "`name` - 模型将在格式化响应中使用的属性名称 \n",
    "\n",
    "`type` - 该属性的数据类型 \n",
    "\n",
    "`description` - 该具体属性的描述 \n",
    "\n",
    "\n",
    "<strong>可选</strong>\n",
    "\n",
    "`required` - 函数调用完成所需的必需属性 \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 调用函数\n",
    "定义函数后，我们需要将其包含在对聊天完成 API 的调用中。我们通过在请求中添加 `functions` 来实现这一点。在这种情况下，`functions=functions`。\n",
    "\n",
    "还有一个选项是将 `function_call` 设置为 `auto`。这意味着我们将允许大型语言模型根据用户消息决定调用哪个函数，而不是自己指定。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "response = client.responses.create(model=deployment, \n",
    "                                        input=messages,\n",
    "                                        tools=functions, \n",
    "                                        tool_choice=\"auto\",\n",
    "                                        store=False) \n",
    "\n",
    "print(response.output)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在让我们看看响应，了解它是如何格式化的：\n",
    "\n",
    "{\n",
    "  \"role\": \"assistant\",\n",
    "  \"function_call\": {\n",
    "    \"name\": \"search_courses\",\n",
    "    \"arguments\": \"{\\n  \\\"role\\\": \\\"student\\\",\\n  \\\"product\\\": \\\"Azure\\\",\\n  \\\"level\\\": \\\"beginner\\\"\\n}\"\n",
    "  }\n",
    "}\n",
    "\n",
    "你可以看到函数名称被调用了，并且根据用户信息，LLM 能够找到适合函数参数的数据。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3.将函数调用集成到应用程序中\n",
    "\n",
    "\n",
    "在我们测试了来自 LLM 的格式化响应之后，现在可以将其集成到应用程序中。\n",
    "\n",
    "### 管理流程\n",
    "\n",
    "要将其集成到我们的应用程序中，请执行以下步骤：\n",
    "\n",
    "首先，调用 Open AI 服务并将消息存储在名为 `response_message` 的变量中。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Extract the function call items from the response output\n",
    "tool_calls = [item for item in response.output if item.type == \"function_call\"]\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在我们将定义一个函数，该函数将调用 Microsoft Learn API 以获取课程列表： \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import requests\n",
    "\n",
    "def search_courses(role, product, level):\n",
    "    url = \"https://learn.microsoft.com/api/catalog/\"\n",
    "    params = {\n",
    "        \"role\": role,\n",
    "        \"product\": product,\n",
    "        \"level\": level\n",
    "    }\n",
    "    response = requests.get(url, params=params)\n",
    "    modules = response.json()[\"modules\"]\n",
    "    results = []\n",
    "    for module in modules[:5]:\n",
    "        title = module[\"title\"]\n",
    "        url = module[\"url\"]\n",
    "        results.append({\"title\": title, \"url\": url})\n",
    "    return str(results)\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "作为最佳实践，我们将先查看模型是否想调用某个函数。之后，我们会创建一个可用的函数，并将其与被调用的函数进行匹配。 \n",
    "然后，我们将函数的参数映射到来自LLM的参数上。\n",
    "\n",
    "最后，我们会附加函数调用消息以及由`search_courses`消息返回的值。这将为LLM提供所有所需信息，\n",
    "以便它使用自然语言响应用户。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Check if the model wants to call a function\n",
    "if tool_calls:\n",
    "    tool_call = tool_calls[0]\n",
    "    print(\"Recommended Function call:\")\n",
    "    print(tool_call.name)\n",
    "    print()\n",
    "\n",
    "    # Call the function. \n",
    "    function_name = tool_call.name\n",
    "\n",
    "    available_functions = {\n",
    "            \"search_courses\": search_courses,\n",
    "    }\n",
    "    function_to_call = available_functions[function_name] \n",
    "\n",
    "    function_args = json.loads(tool_call.arguments)\n",
    "    function_response = function_to_call(**function_args)\n",
    "\n",
    "    print(\"Output of function call:\")\n",
    "    print(function_response)\n",
    "    print(type(function_response))\n",
    "\n",
    "\n",
    "    # Add the model's function call item(s) and our function result to the conversation.\n",
    "    # The Responses API represents tool results as `function_call_output` items.\n",
    "    messages.extend(response.output)  # adding the model's function_call item(s)\n",
    "    messages.append( # adding function response to messages\n",
    "        {\n",
    "            \"type\": \"function_call_output\",\n",
    "            \"call_id\": tool_call.call_id,\n",
    "            \"output\": function_response,\n",
    "        }\n",
    "    )\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在我们将发送更新后的消息给 LLM，以便我们能够收到自然语言的响应，而不是 API JSON 格式的响应。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Messages in next request:\")\n",
    "print(messages)\n",
    "print()\n",
    "\n",
    "second_response = client.responses.create(\n",
    "    input=messages,\n",
    "    model=deployment,\n",
    "    tools=functions,\n",
    "    tool_choice=\"auto\",\n",
    "    temperature=0,\n",
    "    store=False,\n",
    "        )  # get a new response from the model where it can see the function response\n",
    "\n",
    "\n",
    "print(second_response.output_text)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 代码挑战\n",
    "\n",
    "干得好！为了继续学习 Azure Open AI 函数调用，你可以构建：https://learn.microsoft.com/training/support/catalog-api-developer-reference?WT.mc_id=academic-105485-koreyst\n",
    " - 函数的更多参数可能帮助学习者找到更多课程。你可以在这里找到可用的 API 参数：\n",
    " - 创建另一个函数调用，获取学习者的更多信息，比如他们的母语\n",
    " - 当函数调用和/或 API 调用没有返回合适的课程时，创建错误处理\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.2"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}