{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Expense Claim Analysis\n", "\n", "This notebook demonstrates how to create agents that use plugins to process travel expenses from local receipt images, generate an expense claim email, and visualize expense data using a pie chart. Agents dynamically choose functions based on the task context.\n", "\n", "Steps:\n", "1. OCR Agent processes the local receipt image and extracts travel expense data.\n", "2. Email Agent generates an expense claim email.\n", "\n", "### Example of a travel expense scenario:\n", "Imagine you're an employee traveling for a business meeting in another city. Your company has a policy to reimburse all reasonable travel-related expenses. Here's a breakdown of potential travel expenses:\n", "- Transportation:\n", "Airfare for a round trip from your home city to the destination city.\n", "Taxi or ride-hailing services to and from the airport.\n", "Local transportation in the destination city (like public transit, rental cars, or taxis).\n", "\n", "- Accommodation:\n", "Hotel stay for three nights at a mid-range business hotel close to the meeting venue.\n", "\n", "- Meals:\n", "Daily meal allowance for breakfast, lunch, and dinner, based on the company's per diem policy.\n", "\n", "- Miscellaneous Expenses:\n", "Parking fees at the airport.\n", "Internet access charges at the hotel.\n", "Tips or small service charges.\n", "\n", "- Documentation:\n", "You submit all receipts (flights, taxis, hotel, meals, etc.) and a completed expense report for reimbursement." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Import required libraries\n", "\n", "Import the necessary libraries and modules for the notebook." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import logging\n", "logging.getLogger(\"agent_framework.azure\").setLevel(logging.ERROR)\n", "\n", "import os\n", "import base64\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.azure import AzureAIProjectAgentProvider\n", "from azure.identity import AzureCliCredential" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "provider = AzureAIProjectAgentProvider(credential=AzureCliCredential())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " ## Define Expense Models\n", "\n", " Create a Pydantic model for individual expenses and an ExpenseFormatter class to convert a user query into structured expense data.\n", "\n", " Each expense will be represented in the format:\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": [ "## Defining Tools - Generating the Email\n", "\n", "Create a tool function to generate an email for submitting an expense claim.\n", "- This tool uses the `@tool` decorator from the Microsoft Agent Framework.\n", "- It calculates the total amount of the expenses and formats the details into an email body." ] }, { "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": [ "# Tool for Extracting Travel Expenses from Receipt Images\n", "\n", "Create a tool function to extract travel expenses from receipt images.\n", "- This tool uses the `@tool` decorator from the Microsoft Agent Framework.\n", "- It reads the receipt image, encodes it as base64, and returns the data URI for the agent to analyze." ] }, { "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": [ "## Processing Expenses\n", "\n", "Define the agents and wire them into a sequential workflow using `WorkflowBuilder`.\n", "- The OCR agent extracts structured expense data from the receipt image using the `load_receipt_image` tool.\n", "- The Email agent takes the extracted data and generates a professional expense claim email using the `generate_expense_email` tool.\n", "- `WorkflowBuilder` with `add_edge` creates a sequential pipeline: OCR Agent → Email Agent." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ocr_agent = await provider.create_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 = await provider.create_agent(\n", " name=\"EmailAgent\",\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": [ "## Main function\n", "\n", "Build the sequential workflow and run it to process the receipt image and generate the expense claim email." ] }, { "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": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "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", "nbformat_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.0" } }, "nbformat": 4, "nbformat_minor": 2 }