{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 费用报销分析\n",
    "\n",
    "本笔记本演示了如何创建使用插件的代理，以处理来自本地收据图像的差旅费用，生成费用报销邮件，并使用饼图可视化费用数据。代理根据任务上下文动态选择函数。\n",
    "\n",
    "步骤：\n",
    "1. OCR代理处理本地收据图像并提取差旅费用数据。\n",
    "2. 邮件代理生成费用报销邮件。\n",
    "\n",
    "### 差旅费用场景示例：\n",
    "假设您是一名为参加另一城市的商务会议而出差的员工。贵公司有一项政策，报销所有合理的与差旅相关的费用。以下是潜在差旅费用的细目：\n",
    "- 交通：\n",
    "从您所在城市往返目的地城市的机票费用。\n",
    "往返机场的出租车或网约车费用。\n",
    "目的地城市内的本地交通（如公共交通、租车或出租车）。\n",
    "\n",
    "- 住宿：\n",
    "在会议场所附近的中档商务酒店住宿三晚。\n",
    "\n",
    "- 餐饮：\n",
    "按公司每日津贴政策提供的早餐、午餐和晚餐的每日餐费补助。\n",
    "\n",
    "- 杂项费用：\n",
    "机场停车费。\n",
    "酒店的上网费用。\n",
    "小费或小额服务费。\n",
    "\n",
    "- 资料：\n",
    "您提交所有收据（机票、出租车、酒店、餐饮等）及完整的费用报销单进行报销。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 导入所需库\n",
    "\n",
    "导入笔记本所需的库和模块。\n"
   ]
  },
  {
   "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 base64\n",
    "import dotenv\n",
    "from typing import Annotated, List\n",
    "\n",
    "from pydantic import BaseModel, Field\n",
    "\n",
    "from agent_framework import tool, AgentResponseUpdate, WorkflowBuilder\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",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " ## 定义费用模型\n",
    "\n",
    " 创建一个用于单个费用的 Pydantic 模型和一个 ExpenseFormatter 类，用于将用户查询转换为结构化费用数据。\n",
    "\n",
    " 每笔费用将以如下格式表示：\n",
    " `{'date': '07-Mar-2025', 'description': 'flight to destination', 'amount': 675.99, 'category': 'Transportation'}`\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Expense(BaseModel):\n",
    "    date: str = Field(..., description=\"Date of expense in dd-MMM-yyyy format\")\n",
    "    description: str = Field(..., description=\"Expense description\")\n",
    "    amount: float = Field(..., description=\"Expense amount\")\n",
    "    category: str = Field(..., description=\"Expense category (e.g., Transportation, Meals, Accommodation, Miscellaneous)\")\n",
    "\n",
    "class ExpenseFormatter(BaseModel):\n",
    "    raw_query: str = Field(..., description=\"Raw query input containing expense details\")\n",
    "    \n",
    "    def parse_expenses(self) -> List[Expense]:\n",
    "        \"\"\"\n",
    "        Parses the raw query into a list of Expense objects.\n",
    "        Expected format: \"date|description|amount|category\" separated by semicolons.\n",
    "        \"\"\"\n",
    "        expense_list = []\n",
    "        for expense_str in self.raw_query.split(\";\"):\n",
    "            if expense_str.strip():\n",
    "                parts = expense_str.strip().split(\"|\")\n",
    "                if len(parts) == 4:\n",
    "                    date, description, amount, category = parts\n",
    "                    try:\n",
    "                        expense = Expense(\n",
    "                            date=date.strip(),\n",
    "                            description=description.strip(),\n",
    "                            amount=float(amount.strip()),\n",
    "                            category=category.strip()\n",
    "                        )\n",
    "                        expense_list.append(expense)\n",
    "                    except ValueError as e:\n",
    "                        print(f\"[LOG] Parse Error: Invalid data in '{expense_str}': {e}\")\n",
    "        return expense_list"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义工具 - 生成电子邮件\n",
    "\n",
    "创建一个工具函数，用于生成提交报销申请的电子邮件。\n",
    "- 此工具使用 Microsoft Agent Framework 中的 `@tool` 装饰器。\n",
    "- 它计算费用总金额并将详情格式化为电子邮件正文。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@tool(approval_mode=\"never_require\")\n",
    "def generate_expense_email(\n",
    "    expense_data: Annotated[str, \"Semicolon-separated expense entries in 'date|description|amount|category' format\"]\n",
    ") -> str:\n",
    "    \"\"\"Generate an email to submit an expense claim to the Finance Team.\"\"\"\n",
    "    formatter = ExpenseFormatter(raw_query=expense_data)\n",
    "    expenses = formatter.parse_expenses()\n",
    "    if not expenses:\n",
    "        return \"No valid expenses found to include in the email.\"\n",
    "    total_amount = sum(e.amount for e in expenses)\n",
    "    email_body = \"Dear Finance Team,\\n\\n\"\n",
    "    email_body += \"Please find below the details of my expense claim:\\n\\n\"\n",
    "    for e in expenses:\n",
    "        email_body += f\"- {e.date} | {e.description}: ${e.amount:.2f} ({e.category})\\n\"\n",
    "    email_body += f\"\\nTotal Amount: ${total_amount:.2f}\\n\\n\"\n",
    "    email_body += \"Receipts for all expenses are attached for your reference.\\n\\n\"\n",
    "    email_body += \"Thank you,\\n[Your Name]\"\n",
    "    return email_body"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 用于从收据图片中提取差旅费用的工具\n",
    "\n",
    "创建一个工具函数，从收据图片中提取差旅费用。\n",
    "- 该工具使用 Microsoft Agent Framework 中的 `@tool` 装饰器。\n",
    "- 它读取收据图片，将其编码为 base64，并返回数据 URI 以供代理分析。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@tool(approval_mode=\"never_require\")\n",
    "def load_receipt_image(\n",
    "    image_path: Annotated[str, \"Path to the receipt image file\"] = \"receipt.jpg\"\n",
    ") -> str:\n",
    "    \"\"\"Load a receipt image and return its base64-encoded data URI for OCR extraction.\"\"\"\n",
    "    try:\n",
    "        with open(image_path, \"rb\") as f:\n",
    "            image_data = base64.b64encode(f.read()).decode(\"utf-8\")\n",
    "        return f\"data:image/jpeg;base64,{image_data}\"\n",
    "    except Exception as e:\n",
    "        error_msg = f\"[LOG] Error loading image '{image_path}': {str(e)}\"\n",
    "        print(error_msg)\n",
    "        return error_msg"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 处理费用\n",
    "\n",
    "使用 `WorkflowBuilder` 定义代理并将它们连接成一个顺序工作流。\n",
    "- OCR代理使用 `load_receipt_image` 工具从收据图像中提取结构化费用数据。\n",
    "- 邮件代理使用 `generate_expense_email` 工具将提取的数据生成专业的费用报销邮件。\n",
    "- 通过 `add_edge` 的 `WorkflowBuilder` 创建一个顺序管道：OCR代理 → 邮件代理。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ocr_agent = client.as_agent(\n",
    "    tools=[load_receipt_image],\n",
    "    name=\"OCRAgent\",\n",
    "    instructions=(\n",
    "        \"You are an expert OCR assistant specialized in extracting structured data from receipt images. \"\n",
    "        \"Use the 'load_receipt_image' tool to load the receipt image, then analyze it and extract \"\n",
    "        \"travel-related expense details in the format: 'date|description|amount|category' separated by semicolons. \"\n",
    "        \"Follow these rules: \"\n",
    "        \"- Date: Convert dates (e.g., '4/4/22') to 'dd-MMM-yyyy' (e.g., '04-Apr-2022'). \"\n",
    "        \"- Description: Extract item names. \"\n",
    "        \"- Amount: Use numeric values (e.g., '4.50' from '$4.50'). \"\n",
    "        \"- Category: Infer from context (e.g., 'Meals' for food, 'Transportation' for travel, \"\n",
    "        \"'Accommodation' for lodging, 'Miscellaneous' otherwise). \"\n",
    "        \"Ignore totals, subtotals, or service charges unless they are itemized expenses. \"\n",
    "        \"If no expenses are found, return 'No expenses detected'. \"\n",
    "        \"Return only the structured data, no additional text.\"\n",
    "    ),\n",
    ")\n",
    "\n",
    "email_agent = client.as_agent(\n",
    "    name=\"EmailAgent\",\n",
    "    tools=[generate_expense_email],\n",
    "    instructions=(\n",
    "        \"You are an expense claim email generator. Take the travel expense data from the previous agent \"\n",
    "        \"(in 'date|description|amount|category' format separated by semicolons) and use the \"\n",
    "        \"'generate_expense_email' tool to produce a professional expense claim email. \"\n",
    "        \"Pass the semicolon-separated expense data directly to the tool.\"\n",
    "    ),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 主函数\n",
    "\n",
    "构建顺序工作流并运行它，以处理收据图像并生成报销邮件。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **注意：** 此工作流当前将收据图像作为 base64 文本传递，大多数聊天模型（包括 gpt-5-mini）不会将其视为图像。\n",
    "> 图像大小可能还会超过模型的上下文窗口。建议使用 Azure AI Vision（或其他 OCR 工具）运行 OCR，并仅传递提取的文本，或重构为将图像作为 `image_url` 消息发送。\n",
    "> 如果你只是想避免上下文错误，可以尝试使用更小的收据图像或具有更大上下文窗口的模型。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "workflow = WorkflowBuilder(start_executor=ocr_agent) \\\n",
    "    .add_edge(ocr_agent, email_agent) \\\n",
    "    .build()\n",
    "\n",
    "prompt = (\n",
    "    \"Please extract the raw text from the receipt image at 'receipt.jpg', \"\n",
    "    \"focusing on travel expenses like dates, descriptions, amounts, and categories \"\n",
    "    \"(e.g., Transportation, Accommodation, Meals, Miscellaneous). \"\n",
    "    \"Then generate a professional expense claim email.\"\n",
    ")\n",
    "\n",
    "last_author = None\n",
    "events = workflow.run(\n",
    "    prompt,\n",
    "    stream=True,\n",
    ")\n",
    "async for event in events:\n",
    "    if event.type == \"output\" and isinstance(event.data, AgentResponseUpdate):\n",
    "        update = event.data\n",
    "        author = update.author_name\n",
    "        if author != last_author:\n",
    "            if last_author is not None:\n",
    "                print()\n",
    "            print(f\"\\n{'='*50}\")\n",
    "            print(f\"# Agent - {author}:\")\n",
    "            print(f\"{'='*50}\")\n",
    "            last_author = author\n",
    "        print(update.text, end=\"\", flush=True)"
   ]
  },
  {
   "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": 2
}