greenie-msft/expense-processor

Queue-triggered AI agent on Azure Functions that reads an expense request in any format, applies a spending policy, and routes it to approve/review/flag queues.

★ 0Forks 0PythonGitHub ↗Compare

README

Azure Functions Hosted Skill: Expense Processor Python

A markdown-first Azure Functions hosted skill for queue-driven expense processing. Its trigger and instructions live in src/agents/expense_processor.agent.md, and Azure Functions handles execution and scale-to-zero.

What it does

  • 🧾 Reads any format: text, email, key/value, or JSON, and normalizes amounts expressed with symbols, currency codes, words, or colloquial units.
  • 📚 Picks the right policy: lists the documents in Blob Storage and selects the one whose scope matches, then reads and applies it through a read-only Connector Namespace MCP server.
  • 🚦 Routes the decision: approve → expense-approved, review → expense-review, flag / FX → expense-flagged.
  • 🔀 Proves it's reasoning: the same normalized 450 USD is auto-approved as travel but sent to review as a client dinner; tighten one policy document and only that category reroutes.

Prerequisites

Quickstart

uv sync --project src
azd auth login
azd up

The deployment seeds the policy documents but does not submit expenses, so a fresh deployment's queues remain empty. Verify the empty output queues:

uv run --project src --no-sync python scripts/read_decision.py --queue all --peek --cloud

Submit the three bundled demo expenses, wait up to a minute, and read the resulting decisions:

uv run --project src --no-sync python scripts/setup_demo.py send-samples
uv run --project src --no-sync python scripts/read_decision.py --queue all --peek --cloud

send-samples skips submission when an output queue already contains a decision. To repeat the demo, receive and remove the existing decisions first, then run send-samples again:

uv run --project src --no-sync python scripts/read_decision.py --queue all --cloud

You should then see:

Request as received Amount inferred Policy Queue
“four hundred and fifty dollars” flight 450 USD travel-policy.md expense-approved
USD 450.00 monitor 450 USD equipment-software-policy.md expense-approved
$450 client dinner 450 USD meals-entertainment-policy.md expense-review

Open the Application Insights overview, then use Search (formerly Transaction search) or Logs to inspect the hosted skill, model calls, and tool spans:

azd monitor --overview

Look for successful execute_tool azureblob_ListFolder_V4, execute_tool azureblob_GetFileContentByPath_V2, and execute_tool route_expense_decision spans. See Deploy for the KQL queries that show each run and its complete correlated transaction.

Clean up with azd down --purge.

Run it locally

Install Azurite and Azure Functions Core Tools. Copy src/local.settings.json.sample to src/local.settings.json and set the model endpoint and deployment. No API key is needed; the runtime uses DefaultAzureCredential, so sign in with az login or azd auth login. Policy lookup uses the deployed Connector Namespace because Connector Namespace has no local emulator. After azd provision, copy POLICY_MCP_SERVER_URL from azd env get-values into local settings and leave POLICY_MCP_CLIENT_ID empty so your developer credential is used.

azurite --silent --location .azurite               # terminal A
cd src && uv run func start                         # terminal B
uv run --project src --no-sync python scripts/send_expense.py --file samples/travel.txt   # terminal C
uv run --project src --no-sync python scripts/read_decision.py --queue all --peek

The model call still uses Azure. For setup and Windows help, see Troubleshooting.

How it works

flowchart LR
    msg([raw message<br/>text · JSON · key-value])

    subgraph inbound["Azure Queue Storage · inbound"]
        inq[[expense-requests]]
    end

    subgraph policies["Azure Blob Storage · policies container"]
        pdocs[(travel · meals · equipment<br/>general policy docs)]
    end

    skill{{Expense Processor skill<br/>extract · select · apply · route}}

    subgraph outbound["Azure Queue Storage · outbound"]
        approved[[expense-approved]]
        review[[expense-review]]
        flagged[[expense-flagged]]
    end

    msg --> inq
    inq -->|queue trigger| skill
    pdocs -->|Connector Namespace<br/>Blob MCP tools| skill
    skill --> approved
    skill --> review
    skill --> flagged
Loading

The runtime discovers the hosted skill's Markdown definition. Its front matter defines the queue trigger, and its body contains the instructions. A read-only Azure Blob MCP connector lists and reads policy documents with the Connector Namespace managed identity; one Python tool routes decisions to queues with the Function managed identity.

How it works · Use cases · Customize · Deploy · Troubleshooting

Learn more

License

MIT © Microsoft Corporation.

Contributors

greenie-msftmicrosoft-github-operations[bot]

Issues