diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/.devcontainer/devcontainer.json b/03-Azure/01-04-AI/07_Fraud_Intelligence/.devcontainer/devcontainer.json new file mode 100644 index 000000000..2f47afa8e --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/.devcontainer/devcontainer.json @@ -0,0 +1,64 @@ +{ + // Fraud Intelligence Hack — Codespaces / Dev Container + // Python agents + Microsoft Agent Framework orchestration + Azure tooling. + "name": "Fraud Intelligence Hack", + "image": "mcr.microsoft.com/devcontainers/python:3.12-bookworm", + + "features": { + "ghcr.io/devcontainers/features/azure-cli:1": { + "version": "latest", + "installBicep": true + }, + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "version": "latest", + "moby": true + }, + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "latest" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + }, + "ghcr.io/devcontainers/features/powershell:1": { + "version": "latest" + } + }, + + "customizations": { + "vscode": { + "extensions": [ + // Python + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.debugpy", + "ms-toolsai.jupyter", + // Azure / infra + "ms-azuretools.vscode-docker", + "ms-azuretools.vscode-bicep", + "ms-azuretools.vscode-azurefunctions", + "ms-vscode.powershell", + // AI / Foundry tooling + "ms-windows-ai-studio.windows-ai-studio", + // GitHub Copilot + "GitHub.copilot", + "GitHub.copilot-chat" + ], + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python" + } + } + }, + + // Fraud Alert API / DevUI (8080), Functions (7071), hosted agent (8088), Jupyter (8888) + "forwardPorts": [7071, 8080, 8088, 8888], + "portsAttributes": { + "7071": { "label": "Financial Evidence MCP" }, + "8080": { "label": "Fraud Alert API / DevUI" }, + "8088": { "label": "Foundry Hosted Agent" }, + "8888": { "label": "Jupyter" } + }, + + "postCreateCommand": "bash .devcontainer/post-create.sh", + + "remoteUser": "vscode" +} diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/.devcontainer/post-create.sh b/03-Azure/01-04-AI/07_Fraud_Intelligence/.devcontainer/post-create.sh new file mode 100644 index 000000000..ebe515fdb --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/.devcontainer/post-create.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -euo pipefail + +az bicep install + +if ! command -v azd >/dev/null 2>&1; then + curl -fsSL https://aka.ms/install-azd.sh | bash +fi + +if ! azd ext list 2>/dev/null | grep -q 'microsoft.foundry'; then + azd ext install microsoft.foundry +fi + +if ! command -v func >/dev/null 2>&1; then + sudo npm install --global azure-functions-core-tools@4 --unsafe-perm=true +fi \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/README.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/README.md new file mode 100644 index 000000000..98530a7de --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/README.md @@ -0,0 +1,195 @@ +# Agentic AI Hacks | Fraud Intelligence 🔍 + +> ⚠️ **This repository is under construction.** All information found here is a work in progress and is likely to change at any time without prior notice. + +Welcome to the **Fraud Intelligence Hackathon**! In this workshop you will explore how agentic AI systems can transform financial compliance — moving from slow, manual review to a continuously monitoring, reasoning, and auditing multi-agent solution. You will build a modern, reusable, Python-based solution with Microsoft Foundry, the Microsoft Agent Framework, MCP, Foundry IQ, and Azure observability services. + +--- + +## Introduction + +Fraud moves faster than manual review. Analysts piecing together transactions, customer profiles, internal policies, and regulatory updates by hand are slow and error-prone — genuine fraud slips through while false positives pile up, driving direct losses, the risk of significant fines, and erosion of customer trust. + +An **agentic fraud intelligence system** continuously investigates operations and turns raw activity into evidence-backed decisions. It retrieves historical transaction evidence from **Azure Cosmos DB through an MCP server**, reasons over global, internal, and country-specific AML policies with **Foundry IQ agentic retrieval**, evaluates the rules that apply to both the origin and destination bank account countries, and produces audit-ready investigation reports and operational alerts. + +Unlike earlier rule-based systems limited to rudimentary patterns, agentic systems reason through the *why* behind a flag. The complete workflow runs as a **hosted agent in Microsoft Foundry**, uses the **Microsoft Agent Framework** for orchestration, routes models and MCP servers through the new **AI Gateway tier (preview)**, and emits end-to-end telemetry to **Application Insights and Azure Managed Grafana**. + +While Fraud Intelligence is highly relevant for FSI — where fraud, money laundering, and insider trading draw constant regulatory scrutiny — the hack extends cleanly to any regulated industry. With a diverse audience, the goal is to broaden attendees' thinking: they leave with a **modern, reusable, Python-based component set** they can apply to their own domains. + +## Scenario + +A transaction enters the system. The fraud intelligence workflow must: + +1. **Evidence Enrichment Agent** — use the Financial Evidence MCP to retrieve historical banks, accounts, transactions, and laundering evidence from **Azure Cosmos DB**, then enrich the original transaction without making a compliance decision. +2. **Regulatory Assessment Agent** — use **Foundry IQ agentic retrieval** to apply global AML guidance, internal policies, and the regional regulations relevant to the origin and destination bank account countries. +3. **AML Report Agent** — transform the enriched evidence and regulatory assessment into a professional, audit-ready AML investigation report. +4. **Alert Manager Agent** — run in parallel with the AML Report Agent and use the Fraud Alert Manager MCP to create an operational alert when the regulatory status requires one. +5. **Fraud Intelligence Orchestration** — coordinate the agents with the **Microsoft Agent Framework**, deploy the workflow as a hosted agent in Microsoft Foundry, govern model and MCP traffic through **AI Gateway (preview)**, and emit traces and business metrics through OTLP. + +The image below illustrates the conceptual scenario and agent roles: + +```mermaid +flowchart LR + TX([Transaction]) --> EEA["Evidence Enrichment Agent
Financial Evidence MCP"] + EEA --> RAA["Regulatory Assessment Agent
Foundry IQ agentic retrieval"] + RAA --> FORK{Parallel completion} + FORK --> REPORT["AML Report Agent
Investigation report"] + FORK --> ALERT["Alert Manager Agent
Fraud Alert Manager MCP"] + REPORT --> RESULT([Case outputs]) + ALERT --> RESULT + + COSMOS[(Azure Cosmos DB)] --> FMCP["Financial Evidence MCP"] + FMCP --> EEA + IQ["Foundry IQ
Global + internal + regional policies"] --> RAA + ALERT --> AMCP["Fraud Alert Manager MCP"] + AMCP --> API["Existing Fraud Alert Manager API"] +``` + +--- + +## Architecture + +The hackathon builds a **Python-based, multi-agent Fraud Intelligence system**. Individual Microsoft Foundry agents are composed into a hosted workflow with the Microsoft Agent Framework. Foundry IQ supplies agentic retrieval over AML knowledge, while MCP servers provide access to financial evidence and alert-management actions. The AI Gateway tier (preview) provides a common control plane for model and MCP traffic. + +```mermaid +flowchart TB + DEV["GitHub Codespaces + Copilot
Python development"] --> ORCH + + subgraph Foundry["Microsoft Foundry"] + AGENTS["Evidence Enrichment
Regulatory Assessment
AML Report
Alert Manager"] + ORCH["Agent Framework orchestration
Hosted agent runtime"] + AGENTS <--> ORCH + end + + subgraph Knowledge["Knowledge and evidence"] + COSMOS[("Azure Cosmos DB
Historical AML evidence")] + IQ["Foundry IQ knowledge base
Global + internal + regional rules"] + end + + subgraph Gateway["AI Gateway tier (preview)"] + MODELS["Model gateway
Routing + policies"] + MCPPROXY["Proxied MCP
Financial Evidence MCP"] + MCPAPI["MCP generated from API
Fraud Alert Manager"] + end + + COSMOS --> FINMCP["Financial Evidence MCP"] + FINMCP --> MCPPROXY + ALERTAPI["Fraud Alert Manager API"] --> MCPAPI + IQ --> AGENTS + ORCH --> MODELS + AGENTS --> MCPPROXY + AGENTS --> MCPAPI + + ORCH -- "OTLP traces + metrics" --> APPINSIGHTS["Application Insights"] + APPINSIGHTS --> GRAFANA["Azure Managed Grafana
Technical + business dashboards"] +``` + +### The four agents + +| Agent | Role | Data source / tool | Introduced in | +|---|---|---|---| +| **Evidence Enrichment Agent** | Retrieves historical financial evidence and enriches the original transaction | **Financial Evidence MCP** backed by Azure Cosmos DB | Challenge 2 | +| **Regulatory Assessment Agent** | Validates the transaction against applicable global, internal, origin-country, and destination-country AML rules | **Foundry IQ** agentic retrieval | Challenge 3 | +| **AML Report Agent** | Produces a professional, audit-ready AML investigation report without changing the assessment | Structured output from the Regulatory Assessment Agent | Challenge 4 | +| **Alert Manager Agent** | Creates an operational alert when the supplied regulatory status meets the alerting rule | **Fraud Alert Manager MCP** generated from an existing API | Challenge 5 | + +--- + +## Learning Objectives + +By participating in this hackathon, you will learn how to: + +- Build and integrate an **MCP server backed by Azure Cosmos DB** with a Microsoft Foundry agent +- Configure **Foundry IQ** with global, internal, and regional AML sources and use agentic retrieval for country-aware regulatory assessment +- Compose remote agents with the **Microsoft Agent Framework** and deploy the orchestration as a **hosted agent in Microsoft Foundry** +- Configure the **AI Gateway tier (preview)** for model and MCP traffic, including proxying an existing MCP and creating an MCP from an existing API +- Add end-to-end **OTLP tracing** and business metrics with **Application Insights**, then visualize operational and business outcomes in **Azure Managed Grafana** + +--- + +## Requirements + +To successfully complete this hackathon, you will need: + +- A [GitHub account](https://github.com/signup) with access to [GitHub Codespaces](https://github.com/features/codespaces) and [GitHub Copilot](https://github.com/features/copilot) +- Familiarity with Python programming, including JSON handling and API calls +- Familiarity with Generative AI solutions and Azure services +- An active Azure subscription with **Owner** rights +- Ability to provision Azure resources in `swedencentral` or [another supported region](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/concepts/models?tabs=global-standard%2Cstandard-chat-completions#global-standard-model-availability) + +--- + +## Repository structure + +``` +fraud-intelligence-microhack/ +├── challenges/ # Challenge instructions (challenge-01 … challenge-06) +├── labautomation/ # EMEA MicroHack platform provisioning +├── walkthrough/ +│ ├── challenge-01/ # Challenge 1 reference solution +│ ├── challenge-02/ # Challenge 2 reference solution +│ ├── challenge-03/ # Challenge 3 reference solution +│ ├── challenge-04/ # Challenge 4 reference solution +│ ├── challenge-05/ # Challenge 5 reference solution +│ └── challenge-06/ # Challenge 6 reference solution +└── README.md +``` + +| Path | Who uses it | Purpose | +|---|---|---| +| [`challenges/`](./challenges/) | Attendees | Instructions for the preparation challenge and five build challenges | +| [`labautomation/`](./labautomation/) | Platform / facilitators | Per-lab provisioning invoked by the EMEA MicroHack platform | +| [`walkthrough/`](./walkthrough/) | Facilitators / attendees | Reference solutions for each challenge | + +--- + +## Challenges + +This hackathon starts with one preparation challenge followed by five build challenges. Each build stage extends the same end-to-end fraud intelligence solution, and the agent and orchestration code is Python-based. + +### Challenge structure + +Each challenge follows a consistent structure: + +| Section | Description | +|---|---| +| 🎯 **Objective** | Learning goals for the challenge | +| 🧭 **Context and Background** | Scenario explanation, architecture diagrams, and key concepts | +| ✅ **Tasks** | Step-by-step instructions | +| 🚀 **Go Further** | Optional stretch goals | +| 🛠️ **Troubleshooting** | Common problems and solutions | +| 🧠 **Conclusion** | Recap, key takeaways, and further reading | + +### Challenge list + +| # | Challenge | Description | Duration | +|---|---|---|---| +| **1** | [Prepare the Environment](./challenges/challenge-01.md) | Validate access to the Azure resources, Microsoft Foundry project, models, data store, and development tools used throughout the hack | 30 min | +| **2** | [Build the Evidence Enrichment Agent](./challenges/challenge-02.md) | Build a **Financial Evidence MCP** over Azure Cosmos DB, integrate it with the **Evidence Enrichment Agent**, and validate evidence-backed transaction enrichment | 60 min | +| **3** | [Build the Regulatory Assessment Agent](./challenges/challenge-03.md) | Configure **Foundry IQ** with global, internal, and regional AML sources, then use agentic retrieval to assess rules for both bank account countries | 75 min | +| **4** | [Build and Orchestrate the Investigation](./challenges/challenge-04.md) | Build the **AML Report Agent**, compose the first three agents with the **Microsoft Agent Framework**, and deploy the orchestration as a Foundry hosted agent | 75 min | +| **5** | [Govern Models and MCP Servers](./challenges/challenge-05.md) | Introduce the **AI Gateway tier (preview)**, configure model access, proxy the Financial Evidence MCP, create a new MCP from the Fraud Alert Manager API, and add the parallel **Alert Manager Agent** | 75 min | +| **6** | [Observe Fraud Intelligence](./challenges/challenge-06.md) | Add end-to-end **OTLP tracing**, publish technical and business metrics to **Application Insights**, and build an **Azure Managed Grafana** dashboard for business decision makers | 60 min | + +> **Tip:** While it is possible to rush through the challenges, we encourage you to pause and reflect. Consider how each pattern relates to your own context: what business processes in your environment could benefit from coordinated AI agents? How might agents help orchestrate decisions across teams and systems? + +### Solution walkthroughs + +- [Challenge 1 solution](./walkthrough/challenge-01/solution-01.md) +- [Challenge 2 solution](./walkthrough/challenge-02/solution-02.md) +- [Challenge 3 solution](./walkthrough/challenge-03/solution-03.md) +- [Challenge 4 solution](./walkthrough/challenge-04/solution-04.md) +- [Challenge 5 solution](./walkthrough/challenge-05/solution-05.md) +- [Challenge 6 solution](./walkthrough/challenge-06/solution-06.md) + +--- + +## Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit [https://cla.opensource.microsoft.com](https://cla.opensource.microsoft.com/). + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Trademarks + +This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/baseenv b/03-Azure/01-04-AI/07_Fraud_Intelligence/baseenv new file mode 100644 index 000000000..536c7a6f7 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/baseenv @@ -0,0 +1,3 @@ +rootHome=$(pwd) +walkthroughHome=$rootHome/walkthrough +challengesHome=$rootHome/challenges \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-01.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-01.md new file mode 100644 index 000000000..05f03f98a --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-01.md @@ -0,0 +1,118 @@ +# Challenge 1 - Prepare the Environment + +**[Home](../README.md)** | [Next challenge](challenge-02.md) + +## 🎯 Objective + +Sign in with your assigned lab credentials, verify the pre-provisioned Azure resources, and start the GitHub Codespaces development environment used throughout the Fraud Intelligence MicroHack. + +## 🧭 Context and Background + +The remaining challenges use a pre-provisioned Azure environment and a repository hosted on GitHub. You will work from a GitHub Codespace configured with the tools and dependencies required to build the fraud intelligence solution. + +Complete this setup with the credentials provided in your Hackbox. Use the assigned lab account rather than a personal or work account so that you have access to the correct Azure subscription, resource group, GitHub organization, and repository. + +## ✅ Tasks + +### 1. Sign in to Azure + +Open the [Azure portal](https://portal.azure.com) and sign in with the credentials provided in your Hackbox. + +When prompted to choose an account, select **Use another account** and enter the credentials provided. Do not use your personal or work account. + +![Sign in to the Azure portal with another account](/challenges/images/azureportal.png) + +In the Azure portal, select **Resource groups** from the navigation menu. + +![Resource groups in the Azure portal navigation menu](/challenges/images/resource-groups.png) + +Open the resource group assigned to you and confirm that its resources have been deployed successfully. + +![Resources deployed in the assigned resource group](/challenges/images/resource-group-resources.png) + +Verify that you can access the resources used in the later challenges, including the Microsoft Foundry project, model deployments, Azure Cosmos DB account, and Application Insights resource. + +### 2. Sign in to GitHub + +Open [GitHub](https://github.com) and sign in with the credentials provided in your Hackbox. + +When prompted to choose an account, select **Use another account**. Do not use your personal or work account. + +![Sign in to GitHub with another account](/challenges/images/github-login.png) + +Select **Sign in with your identity provider**, then use the assigned lab account to authenticate. + +Open the GitHub organization assigned to your lab account. + +![GitHub organization selector](/challenges/images/github-organization.png) + +Select the assigned organization, then open the `microhack` repository. Confirm that you can see the repository files and folders. + +![Files and folders in the GitHub repository](/challenges/images/github-repository.png) + +### 3. Create the development environment + +From the repository page, select **Code**, then open the **Codespaces** tab. Select the `...` menu and choose **New with options**. + +![Create a GitHub Codespace with options](/challenges/images/github-codespaces.png) + +For **Dev container configuration**, select **Azure / AI / Fraud Intelligence**, then select **Create codespace**. + +GitHub opens the Codespace in a new browser tab. Wait for the container setup to finish, then confirm that the repository files are visible in the Explorer and that the integrated terminal opens without errors. + +Open a terminal in the Codespace (Terminal > New Terminal) and run the following command to verify that Azure CLI is installed: + +```bash +az --version +``` + +Then sign in to Azure from the Codespace terminal: + +```bash +az login +``` + +Next, initialize a local environment file with the following commands: + +```bash +# init RG with your Resource Group name provided in Hackbox +rg= +DEPLOYMENT=$(az deployment group list -g "$rg" \ + --query "sort_by([?starts_with(name, 'mhh')], &properties.timestamp)[-1].name" \ + -o tsv) + +az deployment group show -g "$rg" -n "$DEPLOYMENT" \ + --query properties.outputs -o json | +jq -r 'to_entries[] | + "\(.key)=\(.value.value | + (if type == "string" then . else tojson end) | @sh)"' \ + > hackenv +az deployment group show -g "$rg" -n "$DEPLOYMENT" \ + --query properties.parameters -o json | +jq -r 'to_entries[] + | select(.value.value != null) + | "\(.key)=\(.value.value | + (if type == "string" then . else tojson end) | @sh)"' \ +>> hackenv + +chmod 600 hackenv +echo "rg=$rg" >> hackenv +cat hackenv +source hackenv +source baseenv +``` + +## 🚀 Go Further + +Review the [solution architecture](../README.md#architecture) and identify where each deployed Azure resource is used in the five build challenges. + +## 🛠️ Troubleshooting + +- **The Azure resource group is missing:** Confirm that you signed in with the assigned lab account and selected the assigned subscription in the Azure portal. +- **The GitHub organization or repository is missing:** Sign out of GitHub, then sign in again through **Sign in with your identity provider** using the Hackbox credentials. +- **The dev container configuration is unavailable:** Confirm that you opened the assigned `microhack` repository before creating the Codespace. +- **The Codespace does not finish starting:** Review the creation log for the failing step, then rebuild the container or recreate the Codespace. + +## 🧠 Conclusion + +You have verified the Azure resources and opened the configured development environment. Continue to [Challenge 2](challenge-02.md) to build the Evidence Enrichment Agent. \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-02.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-02.md new file mode 100644 index 000000000..a8a22952a --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-02.md @@ -0,0 +1,235 @@ +# Challenge 2 - Build the Evidence Enrichment Agent + +[Previous challenge](challenge-01.md) | **[Home](../README.md)** | [Next challenge](challenge-03.md) + +## 🎯 Objective + +Load historical AML evidence into Azure Cosmos DB, deploy a read-only **Financial Evidence MCP**, and connect it to an **Evidence Enrichment Agent** in Microsoft Foundry. + +## 🧭 Context and Background + +The Evidence Enrichment Agent receives a transaction and retrieves relevant banks, accounts, historical transactions, and laundering evidence. It adds traceable facts to the original transaction without making a compliance decision. + +```mermaid +flowchart LR + TX[Transaction JSON] --> AGENT[Evidence Enrichment Agent] + AGENT -->|Calls evidence tools| MCP[Financial Evidence MCP] + MCP -->|Reads evidence| COSMOS[(Azure Cosmos DB)] + COSMOS -->|Returns financial records| MCP + MCP -->|Returns tool results| AGENT + AGENT --> RESULT[Enriched transaction] +``` + +In this challenge, you will populate the Cosmos DB evidence store, review and deploy the MCP source code, connect Application Insights to your Foundry project, configure the agent, and inspect its tool calls in the Foundry playground. + +## ✅ Tasks + +### 1. Load the evidence data into Cosmos DB + +The Azure Cosmos DB account and `aml` database were provisioned in Challenge 1. Load the supplied evidence data into that database from your GitHub Codespace. Run the **load-cosmos.sh** script using the following command: + +```bash +"$walkthroughHome/challenge-02/cosmos/load-cosmos.sh" "$cosmosAccountName" +``` + +The script creates the containers as needed and imports the supplied data. A successful run includes output similar to this: + +```text +... +2026-08-26 13:16:48,487 INFO dim-bank complete: 2578 documents +... +2026-08-26 13:17:48,868 INFO fact-laundering-pattern-txn complete: 22743 documents +... +``` + +Open **Data Explorer** for the Cosmos DB account in the Azure portal. Explore documents in the `dim-bank` and `fact-laundering-pattern-txn` containers to understand the structure of the evidence available to the agent. + +### 2. Review and deploy the Financial Evidence MCP + +The **Financial Evidence MCP** provides read-only access to the evidence in Cosmos DB. It exposes tools that allow the Evidence Enrichment Agent to retrieve bank details, account history, individual transactions, and laundering-attempt timelines. + +```mermaid +flowchart LR + AGENT[Evidence Enrichment Agent] --> MCP[Azure Functions MCP endpoint] + + subgraph APP[function_app.py] + BANK[get_bank_information] + ACCOUNT[list_account_transactions] + TX[get_transaction] + ATTEMPT[get_laundering_attempt] + end + + MCP --> BANK & ACCOUNT & TX & ATTEMPT + BANK & ACCOUNT & TX & ATTEMPT --> REPO[CosmosRepository] + REPO --> BANKS[(dim-bank)] + REPO --> TRANSACTIONS[(fact-laundering-pattern-txn)] +``` + +Each MCP tool delegates data access to a shared repository. The repository reads from the appropriate Cosmos DB container and returns structured evidence to the agent. + +Key source files are: + +- `walkthrough/challenge-02/financial-evidence-mcp/src/function_app.py` - the Azure Functions app that implements the MCP tools. +- `walkthrough/challenge-02/financial-evidence-mcp/src/cosmos_repository.py` - the repository that interacts with Cosmos DB. + +Review these files to understand how the tools are implemented and how they interact with Cosmos DB. You can also run the MCP locally with Azure Functions Core Tools before deploying it. + +Deploy the MCP to Azure Functions from your Codespace: + +```bash +"$walkthroughHome/challenge-02/financial-evidence-mcp/deploy.sh" \ + -g "$rg" \ + -a "$cosmosAccountName" \ + -d "aml" \ + -l "$location" +``` + +Follow the progress in the terminal. The script creates the Azure Function App, configures its Cosmos DB settings, grants its managed identity the required read permissions, and publishes the MCP code. + +When deployment finishes, retrieve the MCP endpoint and the `mcp_extension` system key. You will use both values when connecting the MCP to the agent. + +Copy the endpoint printed by the deployment script: + +```text +MCP endpoint: https://.azurewebsites.net/runtime/webhooks/mcp +``` + +Add the endpoint to the `hackenv` file. The following commands retrieve the Function App name and build the full URL: + +```bash +functionAppName=$(az functionapp list --resource-group "$rg" --query "[?starts_with(name, 'mcp-financial-evidence')].name" --output tsv) +echo "financialEvidenceMcpEndpoint=https://$functionAppName.azurewebsites.net/runtime/webhooks/mcp" >> hackenv +``` + +In the Azure portal, open the Function App and select **Functions** > **App keys** > **System keys**. + +![MCP extension system key](/challenges/images/fin-evidence-mcp-key.png) + +Copy the value of the `mcp_extension` key and add it to `hackenv`: + +```bash +echo 'financialEvidenceMcpSystemKey=' >> hackenv +``` + +Load the new environment variables into your shell: + +```bash +source hackenv +``` + +### 3. Create the Evidence Enrichment Agent in Microsoft Foundry + +The agent will use the Financial Evidence MCP to retrieve evidence from Cosmos DB and enrich transactions with traceable facts. + +In the Azure portal, open your **Microsoft Foundry** resource and select **Go to Foundry Portal**. Sign in using your Hackbox credentials. + +![Microsoft Foundry login](/challenges/images/foundry-login.png) + +#### Explore the Foundry project + +The provided Microsoft Foundry project contains two model deployments: a chat model used by the agent and an embedding model. Find both deployments and review their details. + +Next, connect the Application Insights resource deployed in your environment to the Foundry project. This connection allows you to monitor agent performance and investigate issues. + +Open **Manage** and select **Project details** from the left menu. Select **Connected resources**, then **Add connection**. + +![Connected resources in Microsoft Foundry](/challenges/images/foundry-connected-resources.png) + +Select **Application Insights**, then select **Continue**. Choose the Application Insights resource deployed for your lab, leave **API key** as the authentication method, and select **Connect**. + +#### Create the agent + +Under **Build**, open **Agents**, select **New agent**, then select **Build an agent**. + +![Create a new agent in Microsoft Foundry](/challenges/images/foundry-new-agent.png) + +Name the agent `EvidenceEnrichmentAgent`. + +The new agent opens with a blank configuration: + +![Blank agent in Microsoft Foundry](/challenges/images/foundry-blank-agent.png) + +The chat model is selected automatically because it is the only deployed model that can power this agent. The embedding deployment is not a chat model. + +Select **Instructions** and copy the contents of `walkthrough/challenge-02/evidence-enrichment-agent/instructions.md` into the instruction editor. + +Review the instructions before continuing. They define how the agent handles input, invokes tools, reports missing evidence, and avoids making compliance decisions. + +Expand **Tools** and remove the existing **Web Search** tool because it is not an approved evidence source for this agent. + +Select **Add** > **Add tools**, open the **Custom** tab, select **Model Context Protocol**, then select **Create**. + +The MCP configuration form opens: + +![Configure MCP tool in Microsoft Foundry](/challenges/images/foundry-configure-mcp-tool.png) + +Configure these values: + +- **Name:** `financial-evidence-mcp` +- **Remote MCP endpoint:** Use the value returned by `echo "$financialEvidenceMcpEndpoint"`. +- **Authentication:** Leave **Key-based** selected. +- **Key:** `x-functions-key` +- **Value:** Use the value returned by `echo "$financialEvidenceMcpSystemKey"`. + +Select **Connect**. Foundry returns to the agent page and displays the new MCP tool. + +Open the MCP tool's `...` menu and select **Configure**: + +![Configure MCP tool menu in Microsoft Foundry](/challenges/images/foundry-configure-mcp-tool-menu.png) + +Then enable **Always auto-approve all tools**. + +![Configure auto approve for all tools in Microsoft Foundry](/challenges/images/foundry-auto-approve-tools.png) + +This setting allows the agent to use the MCP tools without requesting approval for every call. + +Select **Save**. Foundry creates a new version of the agent. + +### 4. Test and trace the agent + +Open the **Playground** to test the agent. + +![Test the agent in Microsoft Foundry Playground](/challenges/images/foundry-test-agent.png) + +Submit this transaction: + +```json +{ + "transaction_id": "TX-TEST-0001", + "originator_name": "James Carter", + "origin_account": "83D4B1F30", + "bank_origin": "0121", + "beneficiary_name": "Emily Foster", + "destination_account": "818CCA030", + "bank_destination": "29196", + "amount": 15000, + "currency": "EUR" +} +``` + +The response should be a JSON object that contains the original transaction enriched with relevant bank, account, historical transaction, and laundering evidence. + +For this example, the agent should find three pieces of evidence. To inspect the trace, scroll to the bottom and select **Traces**. It shows the agent calling the MCP tools according to its instructions and using the returned evidence to enrich the transaction. + +![Traces showing evidence data returned by the MCP tool](/challenges/images/foundry-traces-evidence.png) + +Select any **Execute tool** span to inspect the data returned by the MCP. + +## 🚀 Go Further + +Test a transaction with incomplete or conflicting evidence. Observe how the agent reports uncertainty and confirm that it does not fabricate evidence or make a compliance decision. + +Run the Financial Evidence MCP locally with Azure Functions Core Tools, invoke each tool directly, and compare the responses with the calls shown in the Foundry trace. + +## 🛠️ Troubleshooting + +- **The Cosmos import fails:** Confirm that `cosmosAccountName` contains the account name, that your Azure identity has Cosmos DB data-plane access, and that the account endpoint is reachable. +- **The MCP deployment fails:** Confirm that `rg`, `location`, and `cosmosAccountName` are loaded from `hackenv`, and verify that Azure Functions Core Tools is installed. +- **The MCP does not connect in Foundry:** Confirm that the endpoint ends with `/runtime/webhooks/mcp` and that the `x-functions-key` value is the `mcp_extension` system key. +- **No MCP tools appear:** Verify that the Function App deployment completed successfully, then reconnect the MCP tool in Foundry. +- **The agent does not call the MCP:** Confirm that the agent instructions were copied completely, Web Search was removed, the MCP tools are auto-approved, and the latest agent version was saved. +- **No traces appear:** Verify that the correct Application Insights resource is connected to the Foundry project, then run the test again. + +## 🧠 Conclusion + +You have built an Evidence Enrichment Agent that retrieves traceable financial evidence from Azure Cosmos DB through a read-only MCP. Continue to [Challenge 3](challenge-03.md), where the enriched transaction will be assessed against global and regional regulations. diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-03.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-03.md new file mode 100644 index 000000000..7c426b2fd --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-03.md @@ -0,0 +1,426 @@ +# Challenge 3 - Build the Regulatory Assessment Agent + +[Previous challenge](challenge-02.md) | **[Home](../README.md)** | [Next challenge](challenge-04.md) + +## 🎯 Objective + +Configure a Foundry IQ knowledge base containing global, internal, and regional AML policies, then build a **Regulatory Assessment Agent** that applies the rules relevant to both sides of an enriched transaction. + +## 🧭 Context and Background + +The Evidence Enrichment Agent from Challenge 2 retrieves facts but does not decide whether a transaction complies with policy. The Regulatory Assessment Agent receives that evidence, determines the origin and destination countries, retrieves the applicable guidance, and returns a cited assessment. + +```mermaid +flowchart LR + TX[Enriched transaction] --> AGENT[Regulatory Assessment Agent] + AGENT --> IQ[Foundry IQ agentic retrieval] + IQ --> GLOBAL[Global AML guidance] + IQ --> INTERNAL[Internal policy] + IQ --> ORIGIN[Origin-country rules] + IQ --> DEST[Destination-country rules] +``` + +The assessment must remain grounded in retrieved policy. Missing or conflicting guidance must be reported explicitly rather than resolved by assumption. + +## ✅ Tasks + +Before starting, source `baseenv` and `hackenv` to load the environment variables used by this challenge. + +```bash +source baseenv +source hackenv +``` + +### 1. Create the Foundry IQ knowledge base + +Build the Foundry IQ knowledge base that will support the Regulatory Assessment Agent. This involves ingesting relevant policy documents, providing retrieval instructions, and ensuring that the documents are searchable during assessments. + +In **Foundry**, open the **Build** section. Then select **Knowledge** in the left navigation pane to open the main interface for managing your knowledge base: + +![Foundry IQ Knowledge](images/foundry-iq-knowledge.png) + +To learn more about how Foundry IQ works, watch the video available on this page. + +At the bottom of the page, select **Create new resource** to start building your knowledge base. + +Use the defaults, accept the acknowledgment, and proceed to create the new knowledge base resource: + +![Foundry IQ Create New Resource](images/foundry-iq-create-new-resource.png) + +Under the hood, a new **Azure AI Search** resource is being created to support the knowledge base. This resource will handle the indexing and retrieval of policy documents, ensuring that the Regulatory Assessment Agent can access the necessary information efficiently. + +After the resource is created, select **Create a knowledge base** and set the following values: + +- **Name**: kb-aml +- **Description**: Synthetic AML / CFT / KYC corpus for Fraud Intelligence +- **Chat completion model**: gpt-5-mini +- **Retrieval reasoning effort**: Minimal +- **Output mode**: Extractive data +- **Retrieval instructions**: You have to always retrieve first the global policies and rules. + +![Foundry IQ Knowledge Base Creation](images/foundry-iq-knowledge-base-creation.png) + +Then select **Add sources** and review the available source types for your knowledge base. + +![Foundry IQ Add Sources](images/foundry-iq-add-sources.png) + +Stop here for now. The next section creates the storage account and containers for the policy documents. + +### 2. Prepare the storage account and containers + +For simplicity, all sources will use **Blob storage**, but each source is managed independently and can be updated or replaced as needed. + +First, create the **Storage Account** in Azure to host the Blob storage and the three containers for the policy documents. + +Run the following Azure CLI command to create the storage account. It uses the `foundryAccountName` environment variable because storage account names must be globally unique: + +```bash +az storage account create --name $foundryAccountName --resource-group $rg --location $location --sku Standard_LRS +``` + +Then, create the three containers in the storage account to store the policy documents: + +```bash +az storage container create --name global --account-name $foundryAccountName +az storage container create --name internal --account-name $foundryAccountName +az storage container create --name regional --account-name $foundryAccountName +``` + +Give appropriate permissions to the storage account and containers to ensure you can upload and manage the policy documents: + +```bash +userId=$(az ad signed-in-user show --query id -o tsv) +storageId=$(az storage account show \ + --name "$foundryAccountName" \ + --resource-group "$rg" \ + --query id -o tsv) + +az role assignment create \ + --assignee-object-id "$userId" \ + --assignee-principal-type User \ + --role "Storage Blob Data Contributor" \ + --scope "$storageId" +``` + +Finally, upload the policy documents into their respective containers. You can use the Azure CLI or Azure Storage Explorer. The `upload-batch` command uploads every file from each local directory: + +```bash +az storage blob upload-batch --account-name "$foundryAccountName" --destination global --source "$walkthroughHome/challenge-03/documents/global" --auth-mode login +az storage blob upload-batch --account-name "$foundryAccountName" --destination internal --source "$walkthroughHome/challenge-03/documents/internal" --auth-mode login +az storage blob upload-batch --account-name "$foundryAccountName" --destination regional --source "$walkthroughHome/challenge-03/documents/regional" --auth-mode login +``` + +To verify that the policy documents were uploaded successfully, open the **Storage Account** in the Azure portal and check the contents of each container: + +![Storage Account Contents](images/storage-account-contents.png) + +Return to the **Foundry IQ** interface to continue adding and managing your policy sources. You can now add the policy sources from the Azure Blob storage containers you created. + +Select **Add Sources**, then choose **Azure Blob Storage** as the source type. + +![Add Azure Blob Storage Source](images/add-azure-blob-storage-source.png) + +For each container, fill in the required details and click **Create** to link it as a policy source. + +The following example shows how to link the `global` container as a policy source: + +- **Name**: ks-global +- **Description**: Global and supranational rules and policies that must be always retrieved +- **Storage account** and **Container**: Specify the storage account name and the container name (`global`) where the policy documents are stored. +- **Authentication type**: API Key +- **Content extraction mode**: Minimal +- **Embedding model**: text-embedding-3-large +- **Chat completion model**: Not needed + +![Link Global Container](images/link-global-container.png) + +The **Status** of the policy source displays **Creating** while it is being linked. During this process, **AI Search** begins indexing the policy documents in the container. To monitor indexing, open the **Search Service** resource and review the following areas: +- **Search management**: go into **Indexes** and **Indexers** to monitor the indexing process. +- **Agentic retrieval**: if you want to see the configuration created from **Foundry IQ**. + + +You will connect the `internal` and `regional` containers later in this guide so that you can compare the output and observe how the engine queries one or more sources. + + +### 3. Create the Regulatory Assessment Agent + +In the previous challenge, you created the `EvidenceEnrichmentAgent`. Now, create the `RegulatoryAssessmentAgent` to assess regulatory compliance using the policy sources in **Foundry IQ**. + +Because this is the second agent, follow the same **Microsoft Foundry** agent creation process you used for the `EvidenceEnrichmentAgent`. + +Relevant steps you should not miss include: + +- Define the agent's name as `RegulatoryAssessmentAgent` +- Set the model: `gpt-5.6-luna` +- Set instructions: find them under `walkthrough/challenge-03/regulatory-assessment-agent/instructions.md`. Review and validate content. +- Remove the `Web Search` tool, irrelevant for this agent. + +Next, add **Knowledge** by selecting **Add**, then **Connect to Foundry IQ**: + +![Connect to Foundry IQ](images/connect-to-foundry-iq.png) + +Select the **Knowledge Base** you created (`kb-aml`), then select **Connect**: + +![Connect to Knowledge Base](images/connect-to-knowledge-base.png) + +Finally, select **Save** to create the new agent and make it ready for use. + +Ensure that the agent has permission to access the connected **Foundry IQ** knowledge base by assigning the `Search Index Data Reader` role to the **Microsoft Foundry Project** managed identity: + +```bash +subscriptionId=$(az account show --query id -o tsv) + +projectId="/subscriptions/$subscriptionId/resourceGroups/$rg/providers/Microsoft.CognitiveServices/accounts/$foundryAccountName/projects/$foundryProjectName" + +projectPrincipalId=$(az resource show \ + --ids "$projectId" \ + --query identity.principalId \ + -o tsv) + +searchId=$(az search service list \ + --resource-group "$rg" \ + --query "[0].id" \ + -o tsv) + +az role assignment create \ + --assignee-object-id "$projectPrincipalId" \ + --assignee-principal-type ServicePrincipal \ + --role "Search Index Data Reader" \ + --scope "$searchId" +``` + +### 4. Test the agent with the global policy source + +Test the agent as you tested the `EvidenceEnrichmentAgent`. Use output from the previous agent as input. You can run the `EvidenceEnrichmentAgent` and save its output, or use the sample below. + +This is an example JSON output from the `EvidenceEnrichmentAgent`: + +```json +{ + "transaction_id": "TX-TEST-0001", + "original_transaction": { + "transaction_id": "TX-TEST-0001", + "originator_name": "James Carter", + "origin_account": "83D4B1F30", + "bank_origin": "0121", + "beneficiary_name": "Emily Foster", + "destination_account": "818CCA030", + "bank_destination": "29196", + "amount": 15000, + "currency": "EUR" + }, + "data_agent_response": { + "transaction_id": "TX-TEST-0001", + "match_found": true, + "assessment": "Historical laundering records were found for one or more accounts in the transaction.", + "origin_account": { + "originator_name": "James Carter", + "account": "83D4B1F30", + "bank_id": "0121", + "bank_name": "Israel Bank #35", + "country": "Israel", + "historical_participation": true, + "role": "Receiver", + "laundering_transaction_count": 3, + "laundering_attempt_count": 3, + "first_seen": "2026-09-02T10:03:00Z", + "last_seen": "2026-09-18T05:02:00Z", + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "evidence": [ + { + "pattern_txn_sk": 120259084422, + "attempt_id": 2502, + "pattern_type": "GATHER-SCATTER", + "step_in_attempt": 5, + "date_key": 20260918, + "txn_ts": "2026-09-18T05:02:00Z", + "from_bank_id": "0015", + "from_account": "84221F9F0", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 44947.3, + "receiving_currency": "Shekel", + "amount_paid": 44947.3, + "payment_currency": "Shekel", + "payment_format": "ACH", + "is_laundering": 1, + "id": "120259084422", + "partition_key": "2502" + }, + { + "pattern_txn_sk": 68719477003, + "attempt_id": 1385, + "pattern_type": "FAN-OUT", + "step_in_attempt": 1, + "date_key": 20260909, + "txn_ts": "2026-09-09T02:52:00Z", + "from_bank_id": "0015", + "from_account": "84221D110", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 23427.81, + "receiving_currency": "Shekel", + "amount_paid": 23427.81, + "payment_currency": "Shekel", + "payment_format": "ACH", + "is_laundering": 1, + "id": "68719477003", + "partition_key": "1385" + }, + { + "pattern_txn_sk": 8589934791, + "attempt_id": 150, + "pattern_type": "BIPARTITE", + "step_in_attempt": 13, + "date_key": 20260902, + "txn_ts": "2026-09-02T10:03:00Z", + "from_bank_id": "0220", + "from_account": "8000EB430", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 58403.73, + "receiving_currency": "Shekel", + "amount_paid": 58403.73, + "payment_currency": "Shekel", + "payment_format": "ACH", + "is_laundering": 1, + "id": "8589934791", + "partition_key": "150" + } + ] + }, + "destination_account": { + "beneficiary_name": "Emily Foster", + "account": "818CCA030", + "bank_id": "29196", + "bank_name": "Bank of Topeka", + "country": "Bangladesh", + "historical_participation": false, + "role": null, + "laundering_transaction_count": 0, + "laundering_attempt_count": 0, + "first_seen": null, + "last_seen": null, + "pattern_types": [], + "evidence": [] + } + }, + "historical_context": { + "accounts_with_history": 1, + "accounts_without_history": 1, + "highest_historical_context_level": 4, + "transaction_interpretation": "One account involved in the transaction has historical AML participation." + }, + "origin_account_enrichment": { + "historical_context_level": 4, + "historical_context_category": "Repeated Participation Across Multiple Patterns", + "participation_consistency": "Receiver Only", + "participation_frequency": "Repeated", + "pattern_diversity_count": 3, + "pattern_diversity_level": "High", + "days_since_last_seen": 10, + "recency_band": "Recent", + "investigator_interpretation": "The account shows repeated historical participation spanning multiple laundering patterns." + }, + "destination_account_enrichment": { + "historical_context_level": 0, + "historical_context_category": "No Historical AML Participation", + "participation_consistency": "None", + "participation_frequency": "None", + "pattern_diversity_count": 0, + "pattern_diversity_level": "None", + "days_since_last_seen": null, + "recency_band": "None", + "investigator_interpretation": "No historical AML participation was identified." + }, + "derived_features": { + "origin_has_history": true, + "destination_has_history": false, + "multiple_pattern_presence": true, + "recent_historical_activity": true, + "highest_historical_context_level": 4 + }, + "aml_regulatory_inputs": { + "historical_context_level": 4, + "pattern_diversity_count": 3, + "laundering_attempt_count": 3, + "recent_participation": true, + "historical_participation_role": "Receiver", + "historical_participation_detected": true + } +} +``` + +The output contains the enrichment details from the previous agent and additional context provided by the `global` policy source. Expand all evaluated files by selecting **+NN** at the bottom of the response: + +![Expand all files](images/foundry-iq-global.png) + +Confirm that all files come from the `global` policy source by checking for `global` in each path. + +Next, add the internal and regional policy sources. + + +### 5. Add the internal and regional policy sources + +Return to **Knowledge** and select the `kb-aml` knowledge base you created. + +First, modify the **Retrieval instructions** to include the internal and regional policy sources after the global policies: + +```text +You have to always retrieve first the global policies and rules, then the internal rules and policies, and finally the regional rules and policies based on the transaction's bank origin and destination. Only the ones for these countries if documentation exists. +``` + +Then, add new **Azure Blob Storage** sources for the `internal` and `regional` containers. Repeat the process you followed for the `global` container, specifying the appropriate storage account, container, and authentication details for each. + +**Internal** + +- **Name**: ks-internal +- **Description**: Internal rules and policies +- **Storage account** and **Container**: Specify the storage account name and the container name (`internal`) where the policy documents are stored. +- **Authentication type**: API Key +- **Content extraction mode**: Minimal +- **Embedding model**: text-embedding-3-large +- **Chat completion model**: Not needed + +**Regional** + +- **Name**: ks-regional +- **Description**: Regional rules and policies +- **Storage account** and **Container**: Specify the storage account name and the container name (`regional`) where the policy documents are stored. +- **Authentication type**: API Key +- **Content extraction mode**: Minimal +- **Embedding model**: text-embedding-3-large +- **Chat completion model**: Not needed + + +Select **Save** to apply the changes to the knowledge base. Wait for all three sources to be indexed before testing the agent. Each source should display a status of **Active**. + +You do not need to modify the agent. The **Knowledge Base** provides a centralized source of domain knowledge, and the agent automatically uses updated information from all sources in the knowledge base. + +Finally, test the agent again and verify that its response and sources reflect the addition of the internal and regional policy sources. For example, if you use the JSON provided earlier, you should now see files for the transaction's origin and destination countries: + +![alt text showing the updated agent response with internal and regional policy sources](images/foundry-iq-regional.png) + +You can also select **Traces** to review the agent's queries and how it retrieved the sources: + +![alt text showing the agent's trace of queries and source retrieval process](images/foundry-iq-traces.png) + +## 🚀 Go Further + +Test a cross-border transaction for which one country has incomplete guidance. Compare broad retrieval queries with metadata-filtered queries and explain which approach produces the most defensible assessment. + +## 🛠️ Troubleshooting + +- **Relevant policies are not retrieved:** Check document ingestion, country metadata, index status, and the wording of the retrieval query. +- **Only one country is assessed:** Confirm that the agent derives and queries both the origin and destination countries. +- **Citations are missing:** Verify that citations are enabled and that the instructions require citations to survive into the structured output. +- **The agent invents a rule:** Strengthen the grounding instructions and require an explicit `insufficient_evidence` outcome when retrieval does not support a decision. +- **The next agent cannot parse the result:** Validate the response against the agreed assessment schema and remove explanatory text outside the structured result. + +## 🧠 Conclusion + +You have added a policy-grounded Regulatory Assessment Agent that evaluates global, internal, and country-specific AML requirements. Continue to [Challenge 4](challenge-04.md) to generate an investigation report and orchestrate the first three agents. diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-04.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-04.md new file mode 100644 index 000000000..bec19086f --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-04.md @@ -0,0 +1,956 @@ +# Challenge 4 - Build and Orchestrate the Investigation + +[Previous challenge](challenge-03.md) | **[Home](../README.md)** | [Next challenge](challenge-05.md) + +## 🎯 Objective + +Build an **AML Report Agent**, compose it with the Evidence Enrichment and Regulatory Assessment agents using the **Microsoft Agent Framework**, and deploy the workflow as a hosted agent in Microsoft Foundry. + +## 🧭 Context and Background + +The first two agents produce evidence and a regulatory decision. The AML Report Agent converts those structured inputs into a consistent, audit-ready investigation report without changing the underlying facts or assessment. + +```mermaid +flowchart LR + TX[Transaction] --> ENRICH[Evidence Enrichment Agent] + ENRICH --> ASSESS[Regulatory Assessment Agent] + ASSESS --> REPORT[AML Report Agent] + REPORT --> OUTPUT[Investigation report] + + subgraph HOSTED[Foundry hosted agent] + ENRICH + ASSESS + REPORT + end +``` + +The orchestration owns sequencing, state transfer, failure handling, and the final response. Each specialist agent retains a narrow responsibility. + +## ✅ Tasks + +Before starting, source `baseenv` and `hackenv` to load the environment variables used by this challenge. + +```bash +source baseenv +source hackenv +``` + +### 1. Create the AML Report Agent + +The AML report agent is responsible for generating a comprehensive investigation report based on the assessments provided by the evidence enrichment and regulatory assessment agents. It ensures that all findings are documented in a structured and audit-ready format. + +The report must preserve source identifiers so an analyst can trace each finding back to its evidence or policy. It must also preserve the regulatory status, avoid adding facts, and distinguish missing information from confirmed findings. + +This is the third agent you will build, so follow the same creation and configuration process used for the previous agents. Configure the following settings: + +- Define the agent's name as `AMLReportAgent` +- Set the model: `gpt-5.6-luna` +- Set instructions: find them under `walkthrough/challenge-04/aml-report-agent/instructions.md`. Review and validate content. +- Remove the `Web Search` tool, irrelevant for this agent. + +This agent does not require any tools, so skip the tool configuration step. Select **Save** to create the agent and make it ready for use. + +### 2. Test the AML Report Agent + +Test the AML Report Agent using the same approach as for the previous agents. Verify that it generates a comprehensive, structured investigation report based on the assessments from the Evidence Enrichment and Regulatory Assessment agents. + +If you saved the output from the `RegulatoryAssessmentAgent` test, provide it as input to the `AMLReportAgent` to simulate a complete investigation workflow. Otherwise, use the following JSON: + +```json +{ + "transaction_id": "TX-TEST-0001", + "original_transaction": { + "transaction_id": "TX-TEST-0001", + "originator_name": "James Carter", + "origin_account": "83D4B1F30", + "bank_origin": "0121", + "beneficiary_name": "Emily Foster", + "destination_account": "818CCA030", + "bank_destination": "29196", + "amount": 15000, + "currency": "EUR" + }, + "data_agent_response": { + "transaction_id": "TX-TEST-0001", + "match_found": true, + "assessment": "Historical laundering records were found for one or more accounts in the transaction.", + "origin_account": { + "originator_name": "James Carter", + "account": "83D4B1F30", + "bank_id": "0121", + "bank_name": "Israel Bank #35", + "country": "Israel", + "historical_participation": true, + "role": "Receiver", + "laundering_transaction_count": 3, + "laundering_attempt_count": 3, + "first_seen": "2026-09-02T10:03:00Z", + "last_seen": "2026-09-18T05:02:00Z", + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "evidence": [ + { + "pattern_txn_sk": 120259084422, + "attempt_id": 2502, + "pattern_type": "GATHER-SCATTER", + "step_in_attempt": 5, + "date_key": 20260918, + "txn_ts": "2026-09-18T05:02:00Z", + "from_bank_id": "0015", + "from_account": "84221F9F0", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 44947.3, + "receiving_currency": "Shekel", + "amount_paid": 44947.3, + "payment_currency": "Shekel", + "payment_format": "ACH", + "is_laundering": 1, + "id": "120259084422", + "partition_key": "2502" + }, + { + "pattern_txn_sk": 68719477003, + "attempt_id": 1385, + "pattern_type": "FAN-OUT", + "step_in_attempt": 1, + "date_key": 20260909, + "txn_ts": "2026-09-09T02:52:00Z", + "from_bank_id": "0015", + "from_account": "84221D110", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 23427.81, + "receiving_currency": "Shekel", + "amount_paid": 23427.81, + "payment_currency": "Shekel", + "payment_format": "ACH", + "is_laundering": 1, + "id": "68719477003", + "partition_key": "1385" + }, + { + "pattern_txn_sk": 8589934791, + "attempt_id": 150, + "pattern_type": "BIPARTITE", + "step_in_attempt": 13, + "date_key": 20260902, + "txn_ts": "2026-09-02T10:03:00Z", + "from_bank_id": "0220", + "from_account": "8000EB430", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 58403.73, + "receiving_currency": "Shekel", + "amount_paid": 58403.73, + "payment_currency": "Shekel", + "payment_format": "ACH", + "is_laundering": 1, + "id": "8589934791", + "partition_key": "150" + } + ] + }, + "destination_account": { + "beneficiary_name": "Emily Foster", + "account": "818CCA030", + "bank_id": "29196", + "bank_name": "Bank of Topeka", + "country": "Bangladesh", + "historical_participation": false, + "role": null, + "laundering_transaction_count": 0, + "laundering_attempt_count": 0, + "first_seen": null, + "last_seen": null, + "pattern_types": [], + "evidence": [] + } + }, + "historical_context": { + "accounts_with_history": 1, + "accounts_without_history": 1, + "highest_historical_context_level": 4, + "transaction_interpretation": "One account involved in the transaction has historical AML participation." + }, + "origin_account_enrichment": { + "historical_context_level": 4, + "historical_context_category": "Repeated Participation Across Multiple Patterns", + "participation_consistency": "Receiver Only", + "participation_frequency": "Repeated", + "pattern_diversity_count": 3, + "pattern_diversity_level": "High", + "days_since_last_seen": 10, + "recency_band": "Recent", + "investigator_interpretation": "The account shows repeated historical participation spanning multiple laundering patterns." + }, + "destination_account_enrichment": { + "historical_context_level": 0, + "historical_context_category": "No Historical AML Participation", + "participation_consistency": "None", + "participation_frequency": "None", + "pattern_diversity_count": 0, + "pattern_diversity_level": "None", + "days_since_last_seen": null, + "recency_band": "None", + "investigator_interpretation": "No historical AML participation was identified." + }, + "derived_features": { + "origin_has_history": true, + "destination_has_history": false, + "multiple_pattern_presence": true, + "recent_historical_activity": true, + "highest_historical_context_level": 4 + }, + "aml_regulatory_inputs": { + "historical_context_level": 4, + "pattern_diversity_count": 3, + "laundering_attempt_count": 3, + "recent_participation": true, + "historical_participation_role": "Receiver", + "historical_participation_detected": true + }, + "aml_regulatory_assessment": { + "assessment_source": "kb-aml", + "assessment_type": "AML regulation and policy compliance validation", + "regulatory_scope": { + "global_rules_evaluated": true, + "supranational_rules_evaluated": true, + "origin_country_rules_evaluated": true, + "destination_country_rules_evaluated": true, + "cross_border_rules_evaluated": true, + "historical_aml_context_rules_evaluated": true, + "origin_country": "Israel", + "destination_country": "Bangladesh", + "is_cross_border": true + }, + "retrieved_policy_sets": [ + { + "policy_set_id": "GLB-STD-001", + "policy_set_name": "Global AML/CFT Standards - Consolidated", + "scope": "GLOBAL", + "jurisdiction": null, + "summary": "Risk-based AML controls, customer due diligence, enhanced due diligence, wire-transfer transparency, monitoring, reporting and record keeping.", + "source_reference": "GLB-STD-001, pa[[1]]†source】" + }, + { + "policy_set_id": "EU-GL-002", + "policy_set_name": "EU ML/TF Risk Factors Guidelines", + "scope": "SUPRANATIONAL", + "jurisdiction": "European Union", + "summary": "Risk-factor and enhanced-due-diligence guidance retrieved as supranational material; direct applicability to these non-EU jurisdictions is not established by the input.", + "source_reference": "EU[[2]]†source】" + }, + { + "policy_set_id": "IL-REG-016", + "policy_set_name": "Israel - Prohibition on Money Laundering", + "scope": "ORIGIN_COUNTRY", + "jurisdiction": "Israel", + "summary": "Identification, beneficiary and controlling-shareholder declarations, cross-border reporting and suspicious-activity duties.", + "source_reference": "IL-REG-016, pa[[3]]†source】" + }, + { + "policy_set_id": "BD-REG-017", + "policy_set_name": "Bangladesh - Money Laundering Prevention", + "scope": "DESTINATION_COUNTRY", + "jurisdiction": "Bangladesh", + "summary": "Customer identification, suspicious-activity reporting, outward-remittance purpose documentation and foreign-exchange records.", + "source_reference": "BD-REG-017, pa[[4]]†source】" + }, + { + "policy_set_id": "GLB-STD-001-CB", + "policy_set_name": "Global Cross-Border Wire Transfer Controls", + "scope": "CROSS_BORDER", + "jurisdiction": "Israel to Bangladesh", + "summary": "Cross-border transfers require accurate originator and beneficiary information and retention of required information through the payment chain.", + "source_reference": "GLB-STD-001, wire-transfer [[5]]†source】" + }, + { + "policy_set_id": "OPS-GEO-005", + "policy_set_name": "Jurisdiction Risk Matrix and Corridor Rules", + "scope": "CROSS_BORDER", + "jurisdiction": "Israel to Bangladesh", + "summary": "Retrieved country ratings are Israel 2 and Bangladesh 3; the higher rating is relevant to corridor review.", + "source_reference": "OPS-[[6]]†source】" + }, + { + "policy_set_id": "OPS-RULE-021-HIST", + "policy_set_name": "Payment-Level Detection Rules - Prior Involvement", + "scope": "HISTORICAL_AML_CONTEXT", + "jurisdiction": null, + "summary": "Prior reported or otherwise documented AML involvement is a specific historical-context evaluation family.", + "source_reference": "OPS-RULE-021, HIST[[7]]†source】" + }, + { + "policy_set_id": "GLB-TYP-015", + "policy_set_name": "Typology Casebook and Detection Scenarios", + "scope": "HISTORICAL_AML_CONTEXT", + "jurisdiction": null, + "summary": "Multiple typologies and repeated historical participation are relevant indicators, but a single indicator is not evidence of laundering.", + "source_reference": "GLB-[[8]]†source】" + } + ], + "rule_evaluations": [ + { + "rule_id": "GLB-STD-001-RBA", + "rule_name": "Risk-based assessment of geography and transaction profile", + "policy_set_name": "Global AML/CFT Standards - Consolidated", + "scope": "GLOBAL", + "jurisdiction": null, + "rule_summary": "Obliged entities shall assess customer, product, channel and origin/destination geography risks and apply proportionate controls.", + "applies_to_transaction": true, + "compliance_status": "COMPLIANT", + "validation_result": { + "passed": true, + "failed": false, + "missing_required_data": false + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "The input contains origin and destination countries, transaction value and currency, bank identifiers, and substantial historical AML context. These fields support risk-based assessment. They do not establish that all required mitigating controls were completed.", + "kb_source_reference": "GLB-[[9]]†source】" + }, + { + "rule_id": "GLB-STD-001-CDD", + "rule_name": "Customer due diligence and ongoing due diligence", + "policy_set_name": "Global AML/CFT Standards - Consolidated", + "scope": "GLOBAL", + "jurisdiction": null, + "rule_summary": "CDD includes identity verification, beneficial-owner identification, purpose and intended-nature information, and ongoing scrutiny against customer profile and source of funds or wealth.", + "applies_to_transaction": true, + "compliance_status": "INSUFFICIENT_DATA", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "Names and account identifiers are present, but the input does not establish identity verification, beneficial-owner information, transaction purpose, source of funds, source of wealth or customer-profile consistency.", + "kb_source_reference": "GLB-[[10]]†source】" + }, + { + "rule_id": "GLB-STD-001-EDD", + "rule_name": "Enhanced due diligence for complex or unusually large transactions", + "policy_set_name": "Global AML/CFT Standards - Consolidated", + "scope": "GLOBAL", + "jurisdiction": null, + "rule_summary": "Enhanced measures apply to complex or unusually large transactions without an apparent economic or lawful purpose and require additional customer, ownership, purpose and source-of-funds information.", + "applies_to_transaction": true, + "compliance_status": "POTENTIAL_GAP", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "The transaction is cross-border and one account has recent repeated participation across three historical AML patterns. The input does not show whether enhanced due diligence, senior approval, source-of-funds corroboration or purpose validation occurred.", + "kb_source_reference": "GLB-[[11]]†source】" + }, + { + "rule_id": "GLB-STD-001-WIRE", + "rule_name": "Cross-border wire-transfer information completeness", + "policy_set_name": "Global Cross-Border Wire Transfer Controls", + "scope": "CROSS_BORDER", + "jurisdiction": "Israel to Bangladesh", + "rule_summary": "Cross-border transfers must include originator name and account or unique reference, beneficiary name and account or unique reference, plus originator address, national ID, or date and place of birth.", + "applies_to_transaction": true, + "compliance_status": "POTENTIAL_GAP", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "Originator and beneficiary names and account numbers are supplied. Originator address, national identification number, or date and place of birth are absent, and the input does not confirm that required data travelled with the payment.", + "kb_source_reference": "GLB-[[12]]†source】" + }, + { + "rule_id": "GLB-STD-001-MON", + "rule_name": "Transaction monitoring and suspicious-activity escalation", + "policy_set_name": "Global AML/CFT Standards - Consolidated", + "scope": "GLOBAL", + "jurisdiction": null, + "rule_summary": "Institutions must monitor transactions, assess suspicious indicators, escalate promptly and report without delay where suspicion or reasonable grounds to suspect arise.", + "applies_to_transaction": true, + "compliance_status": "POTENTIAL_GAP", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "The enriched data supplies significant monitoring indicators and historical records, but does not state whether this transaction was reviewed, escalated, reported, or documented by the compliance function. The evidence supports a potential control gap, not a finding of non-compliance.", + "kb_source_reference": "GLB-[[13]]†source】" + }, + { + "rule_id": "IL-REG-016-ID", + "rule_name": "Origin-jurisdiction identification and beneficiary declaration", + "policy_set_name": "Israel - Prohibition on Money Laundering", + "scope": "ORIGIN_COUNTRY", + "jurisdiction": "Israel", + "rule_summary": "Identification, official verification, beneficiary declaration and controlling-shareholder declaration are required as specified by the applicable sectoral order.", + "applies_to_transaction": true, + "compliance_status": "INSUFFICIENT_DATA", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "The origin country is Israel and the originating bank is identified. The input does not contain verification records, beneficiary declarations, controlling-shareholder information, applicable sectoral order, or evidence of compliance with any cross-border reporting threshold.", + "kb_source_reference": "IL-[[14]]†source】" + }, + { + "rule_id": "BD-REG-017-REM", + "rule_name": "Destination-jurisdiction remittance purpose and record requirements", + "policy_set_name": "Bangladesh - Money Laundering Prevention", + "scope": "DESTINATION_COUNTRY", + "jurisdiction": "Bangladesh", + "rule_summary": "Outward remittances require an authorised purpose and supporting documentation; foreign-exchange transaction records must be retained.", + "applies_to_transaction": true, + "compliance_status": "INSUFFICIENT_DATA", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "The destination account is in Bangladesh, but the input does not provide an authorised purpose code, supporting documentation, licensed remittance channel, sender relationship, or foreign-exchange record.", + "kb_source_reference": "BD-[[15]]†source】" + }, + { + "rule_id": "OPS-GEO-005-CORRIDOR", + "rule_name": "Cross-border corridor risk review", + "policy_set_name": "Jurisdiction Risk Matrix and Corridor Rules", + "scope": "CROSS_BORDER", + "jurisdiction": "Israel to Bangladesh", + "rule_summary": "The corridor assessment considers both country ratings and applies the higher rating to the transaction.", + "applies_to_transaction": true, + "compliance_status": "COMPLIANT", + "validation_result": { + "passed": true, + "failed": false, + "missing_required_data": false + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "The countries differ, so cross-border review applies. The retrieved matrix rates Israel 2 and Bangladesh 3; the assessment records the higher Bangladesh rating. No call-for-action or sanctions-programme designation was retrieved for either country.", + "kb_source_reference": "OPS-[[16]]†source】" + }, + { + "rule_id": "OPS-RULE-021-HIST-01", + "rule_name": "Prior AML involvement and historical context evaluation", + "policy_set_name": "Payment-Level Detection Rules - Prior Involvement", + "scope": "HISTORICAL_AML_CONTEXT", + "jurisdiction": null, + "rule_summary": "Accounts previously identified in AML activity are subject to a dedicated historical-involvement evaluation family.", + "applies_to_transaction": true, + "compliance_status": "COMPLIANT", + "validation_result": { + "passed": true, + "failed": false, + "missing_required_data": false + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "The origin account has three recorded laundering transactions and attempts, three pattern types, historical context level 4, and recent activity. The historical-context rule therefore applies and the required historical indicators are present.", + "kb_source_reference": "OPS-RULE-021 HIST[[17]]†source】" + }, + { + "rule_id": "GLB-TYP-015-HIGH", + "rule_name": "Multiple historical typology indicators", + "policy_set_name": "Typology Casebook and Detection Scenarios", + "scope": "HISTORICAL_AML_CONTEXT", + "jurisdiction": null, + "rule_summary": "Co-occurring typology indicators have enhanced analytical relevance, while a single indicator alone is not evidence of laundering.", + "applies_to_transaction": true, + "compliance_status": "COMPLIANT", + "validation_result": { + "passed": true, + "failed": false, + "missing_required_data": false + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "The origin account appears across three historical pattern types and has recent repeated participation. These are regulatory monitoring indicators, not a determination that the current transaction constitutes laundering.", + "kb_source_reference": "GLB-[[18]]†source】" + } + ], + "overall_compliance": { + "overall_status": "POTENTIAL_GAP", + "non_compliant_rule_count": 0, + "potential_gap_rule_count": 3, + "insufficient_data_rule_count": 3, + "not_applicable_rule_count": 0, + "compliant_rule_count": 4 + }, + "decision_support": { + "calculation_version": "1.0", + "calculation_inputs": { + "highest_historical_context_level": 4, + "non_compliant_rule_count": 0, + "potential_gap_rule_count": 3, + "insufficient_data_rule_count": 3, + "not_applicable_rule_count": 0, + "compliant_rule_count": 4 + }, + "investigation_priority": { + "base_historical_context_score": 50, + "potential_gap_points": 30, + "insufficient_data_points": 15, + "non_compliance_points": 0, + "raw_score": 95, + "score": 95, + "maximum_score": 100, + "priority_band": "Critical" + }, + "workflow_classification": { + "final_verdict": "ENHANCED REVIEW REQUIRED", + "matched_rule_id": "LEVEL_C", + "rationale": [ + "The highest historical context level is 4.", + "Three potential regulatory gaps were identified.", + "Three applicable rules could not be fully assessed because required data was absent.", + "No retrieved rule was assessed as non-compliant." + ], + "meaning": "The transaction has substantial historical AML relevance and unresolved regulatory validation gaps. The classification is a workflow result and does not establish a legal breach or money laundering.", + "is_legal_conclusion": false, + "establishes_money_laundering": false + } + }, + "regulatory_interpretation": { + "summary": "The transaction is regulatory-relevant because it is cross-border from Israel to Bangladesh and the originating account has recent, repeated historical AML participation across three pattern types. The retrieved rules do not support a finding of non-compliance on the available evidence. However, CDD, enhanced-due-diligence, wire-transfer, monitoring, Israeli reporting, and Bangladesh remittance-purpose fields are incomplete or unverified.", + "key_findings": [ + "The origin account has historical AML participation with three recorded laundering transactions and attempts.", + "The origin account has historical participation across BIPARTITE, FAN-OUT and GATHER-SCATTER patterns.", + "The destination account has no historical AML participation in the supplied enrichment.", + "The transaction is cross-border because the origin and destination countries differ.", + "The retrieved jurisdiction matrix rates Israel 2 and Bangladesh 3; no sanctions or call-for-action designation was retrieved for either country.", + "Originator and beneficiary names and account identifiers are present, but address or equivalent originator identification data is absent.", + "CDD, source-of-funds, transaction-purpose, sanctions-screening, enhanced-review and reporting-status evidence is absent." + ], + "regulatory_relevance": "The available evidence supports enhanced regulatory scrutiny and leaves multiple control-validation gaps. It does not establish fraud, money laundering, suspicious activity, or a mandatory operational outcome. Sources: GLB-[[19]]†source】, IL-[[20]]†source】, BD-[[21]]†source】." + }, + "missing_data": [ + { + "field": "identity_verification_status", + "required_for_rule": "GLB-STD-001-CDD and IL-REG-016-ID", + "reason": "Names and account identifiers do not demonstrate verification using reliable independent documents or electronic verification." + }, + { + "field": "beneficial_owner_and_controlling_shareholder_information", + "required_for_rule": "GLB-STD-001-CDD and IL-REG-016-ID", + "reason": "The input does not identify or verify beneficial ownership or controlling-shareholder declarations." + }, + { + "field": "transaction_purpose_and_intended_nature", + "required_for_rule": "GLB-STD-001-CDD and BD-REG-017-REM", + "reason": "No purpose code, economic rationale or intended-nature information is supplied." + }, + { + "field": "source_of_funds_and_source_of_wealth", + "required_for_rule": "GLB-STD-001-CDD and GLB-STD-001-EDD", + "reason": "The input does not contain source-of-funds or source-of-wealth information or corroboration." + }, + { + "field": "originator_address_or_national_id_or_date_and_place_of_birth", + "required_for_rule": "GLB-STD-001-WIRE", + "reason": "The retrieved cross-border wire rule requires one of these originator data elements, which is absent." + }, + { + "field": "wire_transfer_information_propagation_status", + "required_for_rule": "GLB-STD-001-WIRE", + "reason": "The input does not confirm that required originator and beneficiary information remained with the payment through the chain." + }, + { + "field": "enhanced_due_diligence_and_senior_approval_status", + "required_for_rule": "GLB-STD-001-EDD", + "reason": "The input does not show whether enhanced review or senior approval was performed in light of the historical context and transaction profile." + }, + { + "field": "transaction_monitoring_review_and_escalation_status", + "required_for_rule": "GLB-STD-001-MON", + "reason": "No compliance review, escalation, report filing, or documented disposition is supplied." + }, + { + "field": "israel_sectoral_order_and_cross_border_reporting_status", + "required_for_rule": "IL-REG-016-ID", + "reason": "The applicable sectoral order and any required cross-border report status are not provided." + }, + { + "field": "bangladesh_authorised_remittance_purpose_code_and_supporting_documentation", + "required_for_rule": "BD-REG-017-REM", + "reason": "The destination-country policy requires an authorised purpose and supporting documentation for outward remittance records." + }, + { + "field": "sanctions_screening_result", + "required_for_rule": "Global sanctions-screening controls", + "reason": "No sanctions-screening result is present in the input. No specific sanctions match can be determined." + } + ], + "downstream_inputs": { + "has_regulatory_non_compliance": false, + "has_potential_regulatory_gap": true, + "requires_case_recommendation_review": true, + "highest_historical_context_level": 4, + "rules_triggered": [ + "GLB-STD-001-RBA", + "GLB-STD-001-CDD", + "GLB-STD-001-EDD", + "GLB-STD-001-WIRE", + "GLB-STD-001-MON", + "IL-REG-016-ID", + "BD-REG-017-REM", + "OPS-GEO-005-CORRIDOR", + "OPS-RULE-021-HIST-01", + "GLB-TYP-015-HIGH" + ], + "countries_evaluated": [ + "Israel", + "Bangladesh" + ], + "policy_scopes_evaluated": [ + "GLOBAL", + "SUPRANATIONAL", + "ORIGIN_COUNTRY", + "DESTINATION_COUNTRY", + "CROSS_BORDER", + "HISTORICAL_AML_CONTEXT" + ] + } + } +} +``` + +The output is a Markdown report summarizing the regulatory compliance gaps and potential issues identified from the two previous agents' input. + +### 3. Review the Agent Framework Orchestration + +Several frameworks are available for orchestrating agents, each with its own approach to communication, task delegation, and data flow. This challenge uses [Agent Framework](https://learn.microsoft.com/en-us/agent-framework/) to coordinate multiple agents. + +First, run the orchestration locally and test it with the `Agent inspector` tool provided by the **Foundry Toolkit** for Visual Studio Code. + +The source code is in the `orchestration` directory under `challenge-04`. Review these key files: + +- `src/main.py`: The main entry point for the orchestration logic. That is where we will initialize and coordinate the various agents involved in the process. +- `azure.yaml`: Configuration file for the agent that is required during the phase of deploying the agent as **Hosted Agent** in **Microsoft Foundry**. + +Ensure that the agents are defined correctly in the `main.py` file and that their names and versions match those running in the Foundry environment. Open the **Agents** section in the portal to verify these values. + +Those names and versions should be set correctly in the `main.py` file: + +![Agents versions](images/agents-versions.png) + +### 4. Configure and Run the Orchestration Locally + +Create an `.env` file in the `src` folder containing the **FOUNDRY_PROJECT_ENDPOINT**. The local orchestration authenticates using the identity signed in to Azure through `az login`. Run the following command to create the file: + +```bash +echo "FOUNDRY_PROJECT_ENDPOINT=https://$foundryAccountName.services.ai.azure.com/api/projects/$foundryProjectName" > $walkthroughHome/challenge-04/orchestration/src/.env +``` + +Validate the `.env` file to ensure that the **FOUNDRY_PROJECT_ENDPOINT** is set correctly: + +```bash +cat $walkthroughHome/challenge-04/orchestration/src/.env +``` + +After setting the endpoint, run the agent orchestration locally using the provided script: + +```bash +$walkthroughHome/challenge-04/orchestration/run-agent.sh +``` + +The script will set up a virtual environment, install the required dependencies, and then run the main orchestration logic. This allows you to test the entire agent workflow locally before deploying it to the hosted environment. + +In the logs, you should see the initialization of each agent: + +![Agents initialization](images/agents-initialization.png) + + +The final trace shows the port on which the agent is listening: + +![Final trace showing the port the agent is listening on](images/orchestration-started.png) + +To test the orchestration locally, select the **Foundry Toolkit** icon in Visual Studio Code and use the **Agent inspector** tool to interact with the running agents: + +![Agent inspector tool in Foundry Toolkit](images/agent-inspector.png) + +Then use the initial JSON request from Challenge 2 to interact with the agents: +```json +{ + "transaction_id": "TX-TEST-0001", + "originator_name": "James Carter", + "origin_account": "83D4B1F30", + "bank_origin": "0121", + "beneficiary_name": "Emily Foster", + "destination_account": "818CCA030", + "bank_destination": "29196", + "amount": 15000, + "currency": "EUR" +} +``` + +Press `Enter` to send the request. Processing may take some time because all three agents run sequentially. + + +The result should be a Markdown report containing the transaction details and the analysis performed by the agents. + +### 5. Deploy to Microsoft Foundry + +Before deploying, ensure that you are signed in to your Azure account. Use the Azure extension in Visual Studio Code for this process. Select the **Azure** icon in the left sidebar to open the extension panel: + +![Azure Extension new account](images/azure-extension-new-account.png) + +Select **Sign in with new account** and enter the credentials for the Azure account provided for the lab. + +Before proceeding, confirm that the correct Azure subscription is selected. Open **View** > **Command Palette**, then search for `Azure: Select Subscriptions` and choose the appropriate subscription: + +![Command Palette](images/command-palette.png) + +Then: + +![Select Subscription in Azure](images/select-azure-subscription.png) + +Return to the **Foundry Toolkit** extension and set your **Foundry project** as the default. Under **My resources**, select **Set Foundry project**, then choose the project used for this lab. + +After setting the default, you can manage your Foundry project resources and deploy the orchestration directly from Visual Studio Code. + +There are multiple ways to deploy your orchestration to Microsoft Foundry. You can use the `azd` command-line tool or deploy directly from the **Foundry Toolkit** extension. + +**Important Note:** due to limitations in how the extension manages source code, move the orchestration source and the `azure.yaml` agent definition to the root of the project directory before deploying. Run the following command: + +```bash +cp -Rf \ + "$walkthroughHome/challenge-04/orchestration/src" \ + "$walkthroughHome/challenge-04/orchestration/azure.yaml" \ + "$rootHome/" +``` + +Use the latter method to deploy directly from the **Foundry Toolkit** extension. Under **Developer Tools**, expand the **Build** section, then select **Deploy to Microsoft Foundry**. + +Follow these steps to deploy your orchestration. Ensure you use: +- **Deployment method**: Code +- **Package mode**: Remote +- **Deploy to**: New agent, as it is the first deployment of this orchestration + +![Deploy to Microsoft Foundry](images/deploy-to-microsoft-foundry.png) + +Finally, review the deployment options before confirming the deployment: + +![Review deployment options](images/review-deployment-options.png) + +Track the deployment progress in the **Output** panel in Visual Studio Code by selecting **Foundry Toolkit** as the output source: + +![Deployment progress in Output panel](images/deployment-progress-in-output-panel.png) + +Once the deployment is complete, you can run a test using the **Hosted Agent Playground**: + +![Hosted Agent Playground](images/hosted-agent-playground.png) + +The new hosted agent also appears under **Agents** in the **Microsoft Foundry** portal, where you can interact with it and test its functionality: + +![Microsoft Foundry Agents](images/microsoft-foundry-hosted-agents.png) + +Test it from the playground as well: + +![Try from the playground](images/try-from-the-playground.png) + +### 6. Review Traces + +Challenge 6 explores this topic in depth. For now, take a preliminary look at the traces generated by your orchestration. + +In the agent playground, select **Traces**: + +![Traces in the agent's playground](images/traces-in-agents-playground.png) + +Open the last trace to inspect the detailed execution flow of your orchestration: + +![Detailed execution flow of the last trace](images/detailed-execution-flow-of-last-trace.png) + +We will explore tracing and metrics in more depth in Challenge 6. For now, this preliminary view is enough to confirm that the three agents ran in the expected sequence. + +## 🚀 Go Further + +Add idempotent case identifiers and checkpointing so a transient failure can resume without repeating completed investigation stages. + +## 🛠️ Troubleshooting + +- **A remote agent cannot be invoked:** Verify the project endpoint, agent name and version, managed identity, and role assignments. +- **A handoff fails validation:** Compare the producing agent's output with the consuming schema and inspect the raw response in the trace. +- **The report changes the decision:** Ensure the report agent receives the assessment as authoritative and has no instruction to reassess it. +- **Local authentication fails:** Sign in with the lab identity and confirm that `DefaultAzureCredential` selects the intended credential. +- **Hosted deployment fails:** Review the build logs, entry point, dependency versions, environment variables, and hosted-agent health status. + +## 🧠 Conclusion + +You have composed evidence enrichment, regulatory assessment, and reporting into a deployable investigation workflow. Continue to [Challenge 5](challenge-05.md) to govern model and MCP traffic and add operational alerting. \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-05.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-05.md new file mode 100644 index 000000000..17655b514 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-05.md @@ -0,0 +1,1003 @@ +# Challenge 5 - Govern Models and MCP Servers + +[Previous challenge](challenge-04.md) | **[Home](../README.md)** | [Next challenge](challenge-06.md) + +## 🎯 Objective + +Introduce the **AI Gateway tier (preview)** for governed model and MCP access, generate an MCP interface from the Fraud Alert Manager API, build an **Alert Manager Agent**, and run alert creation in parallel with report generation. + +## 🧭 Context and Background + +The investigation workflow now produces a decision, but a decision that requires action must also reach the operational alert system. AI Gateway provides a shared policy and observability boundary for model and tool traffic. + +```mermaid +flowchart LR + ASSESS[Regulatory assessment] --> FORK{Parallel completion} + FORK --> REPORT[AML Report Agent] + FORK --> ALERT[Alert Manager Agent] + ALERT --> AMCP[Fraud Alert Manager MCP] + AMCP --> API[Fraud Alert Manager API] + + ORCH[Hosted orchestration] --> GATEWAY[AI Gateway tier] + GATEWAY --> MODEL[Model deployment] + GATEWAY --> FMCP[Financial Evidence MCP] + GATEWAY --> AMCP +``` + +The Alert Manager Agent must treat the regulatory assessment as authoritative. It decides whether to call the alert tool according to an explicit rule, but it does not re-evaluate the transaction. + +## ✅ Tasks + +Before starting, source `baseenv` and `hackenv` to load the environment variables used by this challenge. + +```bash +source baseenv +source hackenv +``` + +### 1. Deploy the AI Gateway + +An **AI Gateway** is a centralized control point for securing and managing interactions among AI agents, models, and external services. It routes requests and responses through a governance layer that can enforce authentication, authorization, rate limiting, and other policies. This helps protect sensitive information, support compliance, and provide observability into AI-driven workflows. + +In Azure, API Management provides the AI Gateway. In this lab, you will deploy a dedicated API Management instance using the preview `AIGateway` SKU, then route model requests and MCP calls through this centralized governance layer. + +#### Create the Gateway + +During the preview, you can create the `AIGateway` SKU through the **AI Gateway portal**, but not through the public Azure CLI or ARM REST API. + +Open the [AI Gateway portal](https://ai.gateway.azure.com/) and sign in with the lab account. Create a new AI Gateway with these settings: + +- **Name**: enter a globally unique name +- **Subscription**: the lab subscription +- **Region**: `swedencentral` +- **Resource group**: the existing lab resource group +- **Enable managed identity** + +![AI Gateway deployment screenshot](images/ai-gateway-deployment.png) + +### 2. Configure Governed Model Access + +#### Import Models + +In the AI Gateway left navigation pane, select **Models**, then select **Add Models**. + +Models can be imported from several sources and providers. For this lab, select models deployed in **Microsoft Foundry**: + +![Import models from Microsoft Foundry](images/import-models-from-microsoft-foundry.png) + +Choose the lab subscription and Foundry resource, then select **Next**: + +![Select subscription and Foundry resource](images/select-subscription-and-foundry-resource.png) + +Keep the default settings, or adjust the names if needed, then select **Create** to import the models into the AI Gateway: + +![Import models into the AI Gateway](images/import-models-into-ai-gateway.png) + +After a successful import, the newly added models appear under **Models**: + +![Models listed under the AI Gateway](images/models-listed-under-ai-gateway.png) + +#### Configure Agents to Access Models Through the AI Gateway + +After importing the models, configure the agents to access them through the AI Gateway. In **Microsoft Foundry**, open **Manage**, select **Resource details**, and then select **Admin-connected models**: + +![Admin-connected models in Microsoft Foundry](images/admin-connected-models-in-microsoft-foundry.png) + +Select **Add** to open the connection dialog. + +Because the **AI Gateway** SKU is still in preview, select **Other source** instead of **Azure API Management**. Enter the following details: + +- **Connection name**: unique identifier, for instance, `aigateway` +- **Base URL** + 1) Get the Gateway models endpoint from the **AI Gateway** portal: + + ![Get the Gateway models endpoint from the AI Gateway portal](images/get-gateway-models-endpoint.png) + + 2) Get the access key. Create new ones: + + ![Create new access key in the AI Gateway portal](images/create-new-access-key.png) + + Copy the access key and keep it for next steps. Click **Next**. + +- **Authentication**: Select **API Key** and enter the access key you obtained in the previous step. As header name, use `api-key`. Then, **Next** + +Finally, select **Add Model** and configure the following values: + +- **Name**: gpt-5.6-luna +- **Display name**: gpt-5.6-luna +- **Version**: leave it empty +- **Format**: OpenAI + +Select **Save** to finish adding the model, then select **Add** to complete the connection. + +The newly added models should now appear under **Admin-connected models** in Microsoft Foundry: + +![Admin-connected models in Microsoft Foundry](images/admin-connected-models-in-microsoft-foundry-after-adding.png) + +Next, add the new admin-connected model to your agents so they can use it through the AI Gateway. + +Open the **Agents** section in Microsoft Foundry, select the agent you want to configure, such as `EvidenceEnrichmentAgent`, and change its **Model** to the newly added admin-connected model: + +![Select the newly added admin-connected model for the agent](images/select-admin-connected-model-for-agent.png) + +Select **Save** to apply the configuration changes. This creates a new agent version. + +Then test the agent. If you configured `EvidenceEnrichmentAgent`, use this JSON payload: + +```json +{ + "transaction_id": "TX-TEST-0001", + "originator_name": "James Carter", + "origin_account": "83D4B1F30", + "bank_origin": "0121", + "beneficiary_name": "Emily Foster", + "destination_account": "818CCA030", + "bank_destination": "29196", + "amount": 15000, + "currency": "EUR" +} +``` + +The agent should use the newly added admin-connected model to return a response based on the transaction details. + +Return to the **AI Gateway** portal to monitor requests and responses for the newly added admin-connected model. Open **Monitoring** and select **Configure telemetry**. + +![Configure telemetry in the AI Gateway portal](images/configure-telemetry-in-ai-gateway-portal.png) + +Use the existing **Application Insights** instance to monitor telemetry. Select the appropriate instance, then select **Next**: + +![Select Application Insights instance for telemetry in the AI Gateway portal](images/select-application-insights-instance-for-telemetry-in-ai-gateway-portal.png) + +Keep **System-assigned managed identity** as the authentication method, review the configuration, and select **Apply**. + +Allow a minute for the configuration to take effect before looking for telemetry. + +Test the agent again, then review the **Monitoring** section in the AI Gateway portal. Telemetry may take a few moments to appear. You should see a metrics view similar to the following: + +![Telemetry graphs in the AI Gateway portal](images/telemetry-graphs-in-ai-gateway-portal.png) + +If time permits, apply the same model changes to all prompt agents. + +#### Apply a Rate-Limiting Policy + +When multiple agents call the same admin-connected model simultaneously, unrestricted usage can degrade the service. Apply a rate-limiting policy to control usage for each caller identity. + +Open **Models**, select `gpt-5.6-luna`, switch to the **Policies** tab, and select **Add policy**: + +![Add rate limiting policy for the admin-connected model](images/add-rate-limiting-policy-for-admin-connected-model.png) + +Select the **Token rate limit** policy: + +![Select Token rate limit policy in the AI Gateway portal](images/select-token-rate-limit-policy-in-ai-gateway-portal.png) + +Set the desired rate-limit parameters. Keep **Caller identity** as the target; in this case, the identity is the key used to connect Microsoft Foundry to the AI Gateway model: + +![Set rate limit parameters in the AI Gateway portal](images/set-rate-limit-parameters-in-ai-gateway-portal.png) + +Select **Create** to apply the rate-limiting policy. + +Requests that exceed the configured limit will now be throttled, helping to maintain fair usage and service availability. + + +### 3. Proxy the Financial Evidence MCP + +The **AI Gateway** can proxy existing Model Context Protocol (MCP) servers. This applies access, authentication, and rate-limiting policies at the gateway without requiring changes to the MCP implementation. + +Next, onboard the existing `financial evidence` MCP to the AI Gateway. In the **AI Gateway** portal, open the **MCP servers** section and select **Add MCP server**: + +![Add MCP server in the AI Gateway portal](images/add-mcp-server-in-ai-gateway-portal.png) + +The portal supports three backend types: + +![Supported backend types in the AI Gateway portal](images/supported-backend-types-in-ai-gateway-portal.png) + +Select **MCP server**, then provide the details required to connect to the existing Financial Evidence MCP: + +- **Backend Name**: A friendly name for the MCP server, for instance, `financial-evidence-mcp`. +- **URL**: The URL of the existing `financial evidence` MCP server. Use the value returned by `echo "$financialEvidenceMcpEndpoint"` +- **Authentication**: set **API Key**, with: + - **Header Name**: `x-functions-key` + - **Value:** Use the value returned by `echo "$financialEvidenceMcpSystemKey"`. + +Select **Next**, validate the configuration, and select **Create** to add the MCP server to the **AI Gateway**. + +You can now test the MCP from the **AI Gateway**. Select **Use**: + +![Test the MCP from the AI Gateway](images/test-mcp-from-ai-gateway.png) + +Select the **Try it** tab, then select **List tools**. You should see all tools exposed by the Financial Evidence MCP: + +![List tools in the financial evidence MCP](images/list-tools-in-financial-evidence-mcp.png) + +Select the **Get Bank Information** operation, enter `0121` as the bank ID, and select **Run tool**: + +![Run Get Bank Information tool in the financial evidence MCP](images/run-get-bank-information-tool-in-financial-evidence-mcp.png) + +The result should display information for bank ID `0121`. Expand the `data` element to view the details: + +![View detailed bank information in the financial evidence MCP](images/view-detailed-bank-information-in-financial-evidence-mcp.png) + +You can now interact with the Financial Evidence MCP through the **AI Gateway**. Next, configure `EvidenceEnrichmentAgent` to use it. + +#### Update the EvidenceEnrichmentAgent Configuration + +Return to the **Microsoft Foundry** portal. Under **Build**, select **Agents**, then select `EvidenceEnrichmentAgent`. Remove the existing tool to ensure that the agent uses the latest MCP configuration: + +![Remove existing tool from the EvidenceEnrichmentAgent](images/remove-existing-tool-from-evidenceenrichmentagent.png) + +Select **Save**, then open the **Tools** menu on the left side of the page. + +Select the `financial-evidence-mcp` tool and update its URL and authentication: + +- **Remote MCP Server endpoint**: Use the Financial Evidence MCP URL exposed through the **AI Gateway**. +- **Credential**: Set the header name to `api-key` and use an existing key from your **AI Gateway** configuration. + +Then select **Update**. + +On the same MCP configuration page, select **Use in an agent**: + +![Use the MCP in an agent](images/use-mcp-in-agent.png) + +Select `EvidenceEnrichmentAgent` as the agent that will use this MCP. + +Under the `EvidenceEnrichmentAgent` **Tools** section, verify that the `financial-evidence-mcp` endpoint shows the new **AI Gateway** configuration. Enable **Always auto-approve all tools**: + +![Configure Always auto-approve all tools](images/configure-always-auto-approve-all-tools.png) + +**Save** the agent again to apply the new configuration. + +Test the agent and inspect its traces to verify that it uses `financial-evidence-mcp` through the **AI Gateway**. + +To explore additional gateway policies, open the **Policies** section for `financial-evidence-mcp`. Because the **AI Gateway** is in preview, some MCP features, including the **Monitoring** tab, may still be unavailable. + + +### 4. Generate an MCP from the Fraud Alert Manager API + +The **AI Gateway** can also expose an existing API as an MCP, bringing agent integration and policy management to APIs that were not originally designed as MCP servers. + +For this scenario, the Fraud Alert Manager API manages financial alerts so that operational users can review them and take action. A simple web interface is also available for validation. + +First, retrieve the API endpoint so that the AI Gateway can read its OpenAPI specification. The API runs on **Azure Container Apps**: + +```bash +endpoint=$(az containerapp show --name alert-manager --resource-group $rg --query properties.configuration.ingress.fqdn -o tsv) +echo "https://$endpoint/openapi.json" +``` +Copy the URL; you will use it to configure the MCP in the **AI Gateway**. + +Return to **MCP servers** in the **AI Gateway** portal. Follow the previous process using a different backend. + +Select **Add MCP server**, choose **OpenAPI Specification**, and complete the form with the following details: + +- **Name**: Provide a name for the MCP server, e.g., `alert-manager-mcp`. +- **Spec source**: Select **Fetch from URL** +- **Spec URL**: Paste the URL you obtained from the previous step. +- **Authentication**: Select **None** because the lab API does not require authentication. + +![Add MCP server](images/add-mcp-server-from-api.png) + +Select **Next**, validate the configuration, and select **Create** to add the MCP server. + +Explore the new MCP server in the **AI Gateway** playground. For example, list the existing alerts: + +![List existing alerts](images/list-existing-alerts.png) + + +Next, create `AlertManagerAgent` to interact with `alert-manager-mcp` through the **AI Gateway**, then add the agent to the full orchestration. + +### 5. Create the AlertManagerAgent + +This is the fourth agent in the workflow. `AlertManagerAgent` uses `alert-manager-mcp` to manage financial alerts through the **AI Gateway**. + +Use the following configuration: + +- Use the agent instructions in `walkthrough/challenge-05/alert-manager-agent/instructions.md`. +- Select the model routed through the **AI Gateway**. +- Onboard the MCP in **Microsoft Foundry** following the usual steps for MCP integration. Remember to add authentication with **API Key**, using header name `api-key` and value a valid API key provided by the **AI Gateway**. +- Configure the MCP for the agent and enable auto-approval for all tools. +- Save the agent configuration. + +The supplied instructions require the agent to accept the regulatory assessment without changing it, create an alert only when the configured status requires one, and surface tool failures without claiming success. The agent sends only the operational data required by the API and uses the transaction ID as a stable case key. + +You can verify alert creation by inspecting the agent traces and confirming that the MCP call succeeded. + +Alternatively, use the Alert Manager web interface to view the current alerts. + +The web interface runs in the same container as the API. Retrieve its URL: + +```bash +endpoint=$(az containerapp show --name alert-manager --resource-group $rg --query properties.configuration.ingress.fqdn -o tsv) +echo "https://$endpoint" +``` + +Open the URL. Before testing, the dashboard contains five alerts: + +![List of current alerts](images/list-of-current-alerts.png) + +Test `AlertManagerAgent` by sending it the following JSON payload: + +```json +{ + "transaction_id": "TX-TEST-0001", + "original_transaction": { + "transaction_id": "TX-TEST-0001", + "originator_name": "James Carter", + "origin_account": "83D4B1F30", + "bank_origin": "0121", + "beneficiary_name": "Emily Foster", + "destination_account": "818CCA030", + "bank_destination": "29196", + "amount": 15000, + "currency": "EUR" + }, + "data_agent_response": { + "transaction_id": "TX-TEST-0001", + "match_found": true, + "assessment": "Historical laundering records were found for one or more accounts in the transaction.", + "origin_account": { + "originator_name": "James Carter", + "account": "83D4B1F30", + "bank_id": "0121", + "bank_name": "Israel Bank #35", + "country": "Israel", + "historical_participation": true, + "role": "Receiver", + "laundering_transaction_count": 3, + "laundering_attempt_count": 3, + "first_seen": "2026-09-02T10:03:00Z", + "last_seen": "2026-09-18T05:02:00Z", + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "evidence": [ + { + "pattern_txn_sk": 120259084422, + "attempt_id": "2502", + "pattern_type": "GATHER-SCATTER", + "step_in_attempt": 5, + "date_key": 20260918, + "txn_ts": "2026-09-18T05:02:00Z", + "from_bank_id": "0015", + "from_account": "84221F9F0", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 44947.3, + "receiving_currency": "Shekel", + "amount_paid": 44947.3, + "payment_currency": "Shekel", + "payment_format": "ACH", + "counterparty_bank_id": "0015", + "counterparty_account": "84221F9F0", + "participation_role": "Receiver" + }, + { + "pattern_txn_sk": 68719477003, + "attempt_id": "1385", + "pattern_type": "FAN-OUT", + "step_in_attempt": 1, + "date_key": 20260909, + "txn_ts": "2026-09-09T02:52:00Z", + "from_bank_id": "0015", + "from_account": "84221D110", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 23427.81, + "receiving_currency": "Shekel", + "amount_paid": 23427.81, + "payment_currency": "Shekel", + "payment_format": "ACH", + "counterparty_bank_id": "0015", + "counterparty_account": "84221D110", + "participation_role": "Receiver" + }, + { + "pattern_txn_sk": 8589934791, + "attempt_id": "150", + "pattern_type": "BIPARTITE", + "step_in_attempt": 13, + "date_key": 20260902, + "txn_ts": "2026-09-02T10:03:00Z", + "from_bank_id": "0220", + "from_account": "8000EB430", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 58403.73, + "receiving_currency": "Shekel", + "amount_paid": 58403.73, + "payment_currency": "Shekel", + "payment_format": "ACH", + "counterparty_bank_id": "0220", + "counterparty_account": "8000EB430", + "participation_role": "Receiver" + } + ] + }, + "destination_account": { + "beneficiary_name": "Emily Foster", + "account": "818CCA030", + "bank_id": "29196", + "bank_name": "Bank of Topeka", + "country": "Bangladesh", + "historical_participation": false, + "role": null, + "laundering_transaction_count": 0, + "laundering_attempt_count": 0, + "first_seen": null, + "last_seen": null, + "pattern_types": [], + "evidence": [] + } + }, + "historical_context": { + "accounts_with_history": 1, + "accounts_without_history": 1, + "highest_historical_context_level": 5, + "transaction_interpretation": "One account involved in the transaction has historical AML participation." + }, + "origin_account_enrichment": { + "historical_context_level": 5, + "historical_context_category": "Extensive Historical Participation", + "participation_consistency": "Receiver Only", + "participation_frequency": "Repeated", + "pattern_diversity_count": 3, + "pattern_diversity_level": "High", + "days_since_last_seen": 12, + "recency_band": "Recent", + "investigator_interpretation": "The account appeared across several AML typologies and multiple independent laundering attempts." + }, + "destination_account_enrichment": { + "historical_context_level": 0, + "historical_context_category": "No Historical AML Participation", + "participation_consistency": "None", + "participation_frequency": "None", + "pattern_diversity_count": 0, + "pattern_diversity_level": "None", + "days_since_last_seen": null, + "recency_band": "None", + "investigator_interpretation": "No historical AML participation was identified." + }, + "derived_features": { + "origin_has_history": true, + "destination_has_history": false, + "multiple_pattern_presence": true, + "recent_historical_activity": true, + "highest_historical_context_level": 5 + }, + "aml_regulatory_inputs": { + "historical_context_level": 5, + "pattern_diversity_count": 3, + "laundering_attempt_count": 3, + "recent_participation": true, + "historical_participation_role": "Receiver", + "historical_participation_detected": true + }, + "aml_regulatory_assessment": { + "assessment_source": "kb-aml", + "assessment_type": "AML regulation and policy compliance validation", + "regulatory_scope": { + "global_rules_evaluated": true, + "supranational_rules_evaluated": true, + "origin_country_rules_evaluated": true, + "destination_country_rules_evaluated": true, + "cross_border_rules_evaluated": true, + "historical_aml_context_rules_evaluated": true, + "origin_country": "Israel", + "destination_country": "Bangladesh", + "is_cross_border": true + }, + "retrieved_policy_sets": [ + { + "policy_set_id": "GLB-STD-001", + "policy_set_name": "Global AML/CFT Standards Summary", + "scope": "GLOBAL", + "jurisdiction": null, + "summary": "Risk-based AML controls, CDD, enhanced due diligence, wire-transfer transparency, monitoring, reporting, sanctions screening and record keeping.", + "source_reference": "01_Global_AML_CFT_Standards_Summary.pdf【6:9†source_name】" + }, + { + "policy_set_id": "GLB-STD-001", + "policy_set_name": "Supranational FATF-style Framework", + "scope": "SUPRANATIONAL", + "jurisdiction": null, + "summary": "Supranational risk-based controls apply using origin and destination geography and relevant transaction indicators.", + "source_reference": "01_Global_AML_CFT_Standards_Summary.pdf【6:9†source_name】" + }, + { + "policy_set_id": "IL-REG-016", + "policy_set_name": "Israel AML Local Regulation", + "scope": "ORIGIN_COUNTRY", + "jurisdiction": "Israel", + "summary": "Israel identification, beneficiary and controlling-shareholder declaration, cross-border reporting and suspicious-activity obligations.", + "source_reference": "16_Israel_AML_Local_Regulation.pdf【6:16†source_name】" + }, + { + "policy_set_id": "BD-REG-017", + "policy_set_name": "Bangladesh AML Local Regulation", + "scope": "DESTINATION_COUNTRY", + "jurisdiction": "Bangladesh", + "summary": "Bangladesh customer identification, suspicious-activity reporting and outward-remittance documentation requirements.", + "source_reference": "17_Bangladesh_AML_Local_Regulation.pdf【6:3†source_name】【6:1†source_name】" + }, + { + "policy_set_id": "GLB-STD-001", + "policy_set_name": "Cross-Border AML and Wire-Transfer Policy", + "scope": "CROSS_BORDER", + "jurisdiction": "Israel to Bangladesh", + "summary": "Cross-border transfers require accurate originator and beneficiary information, risk-based review and applicable jurisdictional controls.", + "source_reference": "01_Global_AML_CFT_Standards_Summary.pdf【6:10†source】【6:11†source】" + }, + { + "policy_set_id": "HIST-AML-001", + "policy_set_name": "Historical AML Participation Review", + "scope": "HISTORICAL_AML_CONTEXT", + "jurisdiction": null, + "summary": "Historical participation and repeated typology indicators are relevant inputs to transaction monitoring and enhanced review.", + "source_reference": "01_Global_AML_CFT_Standards_Summary.pdf【6:6†source】" + } + ], + "rule_evaluations": [ + { + "rule_id": "AML-GLOBAL-CDD-001", + "rule_name": "Customer due diligence and beneficial-owner verification", + "policy_set_name": "Global AML/CFT Standards Summary", + "scope": "GLOBAL", + "jurisdiction": null, + "rule_summary": "CDD applies to relevant transactions and requires identity, beneficial-owner, purpose and ongoing monitoring information.", + "applies_to_transaction": true, + "compliance_status": "INSUFFICIENT_DATA", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 5, + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "recent_historical_activity": true + }, + "interpretation": "Names and account identifiers are present, but the input does not establish identity verification, beneficial-owner verification, purpose, source of funds or source of wealth.", + "kb_source_reference": "01_Global_AML_CFT_Standards_Summary.pdf【6:9†source】【6:10†source】" + }, + { + "rule_id": "AML-GLOBAL-TRM-001", + "rule_name": "Transaction monitoring for historical AML indicators", + "policy_set_name": "Historical AML Participation Review", + "scope": "HISTORICAL_AML_CONTEXT", + "jurisdiction": null, + "rule_summary": "Monitoring must consider co-occurring typology indicators and prior AML participation.", + "applies_to_transaction": true, + "compliance_status": "COMPLIANT", + "validation_result": { + "passed": true, + "failed": false, + "missing_required_data": false + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 5, + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "recent_historical_activity": true + }, + "interpretation": "The evidence contains repeated participation, three pattern types, three attempts and recent activity, providing the historical indicators required for monitoring.", + "kb_source_reference": "01_Global_AML_CFT_Standards_Summary.pdf【6:6†source】" + }, + { + "rule_id": "AML-CROSS-BORDER-WIRE-001", + "rule_name": "Cross-border wire-transfer information", + "policy_set_name": "Cross-Border AML and Wire-Transfer Policy", + "scope": "CROSS_BORDER", + "jurisdiction": "Israel to Bangladesh", + "rule_summary": "Cross-border messages must contain required and accurate originator and beneficiary information.", + "applies_to_transaction": true, + "compliance_status": "INSUFFICIENT_DATA", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 5, + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "recent_historical_activity": true + }, + "interpretation": "Originator and beneficiary names and account identifiers are supplied, but address, national identification or date/place of birth and transfer-message completeness are not provided.", + "kb_source_reference": "01_Global_AML_CFT_Standards_Summary.pdf【6:10†source】【6:11†source】" + }, + { + "rule_id": "AML-IL-001", + "rule_name": "Israel identification and cross-border reporting", + "policy_set_name": "Israel AML Local Regulation", + "scope": "ORIGIN_COUNTRY", + "jurisdiction": "Israel", + "rule_summary": "Relevant Israeli institutions must perform required identification, record the purpose and corridor information, and report suspicious activity where suspicion arises.", + "applies_to_transaction": true, + "compliance_status": "POTENTIAL_GAP", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 5, + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "recent_historical_activity": true + }, + "interpretation": "Israel is the origin jurisdiction and the transfer is cross-border. The input does not show completed identification, declared beneficiary or controlling shareholder, purpose, or reporting assessment.", + "kb_source_reference": "16_Israel_AML_Local_Regulation.pdf【6:4†source】" + }, + { + "rule_id": "AML-BD-001", + "rule_name": "Bangladesh remittance purpose and supporting documentation", + "policy_set_name": "Bangladesh AML Local Regulation", + "scope": "DESTINATION_COUNTRY", + "jurisdiction": "Bangladesh", + "rule_summary": "Outward-remittance controls require authorised purpose and supporting documentation; transaction and suspicious-activity requirements apply where relevant.", + "applies_to_transaction": true, + "compliance_status": "POTENTIAL_GAP", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 5, + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "recent_historical_activity": true + }, + "interpretation": "Bangladesh is the destination jurisdiction, but the input does not provide the transfer purpose, authorised purpose code, licensed channel evidence or supporting documentation.", + "kb_source_reference": "17_Bangladesh_AML_Local_Regulation.pdf【6:1†source】" + }, + { + "rule_id": "AML-GLOBAL-EDD-001", + "rule_name": "Enhanced review for complex or unusually large activity", + "policy_set_name": "Global AML/CFT Standards Summary", + "scope": "SUPRANATIONAL", + "jurisdiction": null, + "rule_summary": "Complex or unusually large transactions without apparent lawful purpose and higher geographic or historical exposure require enhanced due-diligence consideration.", + "applies_to_transaction": true, + "compliance_status": "POTENTIAL_GAP", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 5, + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "recent_historical_activity": true + }, + "interpretation": "The corridor includes Israel and Bangladesh, and one account has extensive recent historical participation across multiple typologies. Enhanced-review evidence such as senior approval, purpose and source-of-funds information is absent.", + "kb_source_reference": "01_Global_AML_CFT_Standards_Summary.pdf【6:9†source】【6:10†source】" + } + ], + "overall_compliance": { + "overall_status": "POTENTIAL_GAP", + "non_compliant_rule_count": 0, + "potential_gap_rule_count": 3, + "insufficient_data_rule_count": 2, + "not_applicable_rule_count": 0, + "compliant_rule_count": 1 + }, + "decision_support": { + "calculation_version": "1.0", + "calculation_inputs": { + "highest_historical_context_level": 5, + "non_compliant_rule_count": 0, + "potential_gap_rule_count": 3, + "insufficient_data_rule_count": 2, + "not_applicable_rule_count": 0, + "compliant_rule_count": 1 + }, + "investigation_priority": { + "base_historical_context_score": 65, + "potential_gap_points": 30, + "insufficient_data_points": 10, + "non_compliance_points": 0, + "raw_score": 105, + "score": 100, + "maximum_score": 100, + "priority_band": "Critical" + }, + "workflow_classification": { + "final_verdict": "ENHANCED REVIEW REQUIRED", + "matched_rule_id": "LEVEL_C", + "rationale": [ + "The highest historical context level is 5.", + "Three potential regulatory gaps were identified.", + "Two applicable rules cannot be fully assessed because required data is absent.", + "No retrieved rule was assessed as clearly non-compliant." + ], + "meaning": "The transaction has substantial regulatory relevance and unresolved validation gaps. This is a workflow classification, not a legal conclusion.", + "is_legal_conclusion": false, + "establishes_money_laundering": false + } + }, + "regulatory_interpretation": { + "summary": "The transaction is cross-border from Israel to Bangladesh and involves an origin account with extensive, recent participation across three historical AML typologies. The available data supports transaction-monitoring validation, but does not establish completion of CDD, enhanced due diligence, wire-transfer data requirements, purpose validation or local reporting controls.", + "key_findings": [ + "The origin account has historical participation level 5, three laundering attempts and three pattern types.", + "The destination account has no recorded historical AML participation.", + "Israel and Bangladesh country rules are applicable because the transaction crosses both jurisdictions.", + "Originator and beneficiary names and account identifiers are present, but additional wire-transfer identity fields are absent.", + "Purpose, source-of-funds, beneficial-owner, sanctions-screening and reporting-status fields are absent.", + "No direct regulatory breach was established from the retrieved rules and supplied evidence." + ], + "regulatory_relevance": "The transaction requires regulatory validation because of cross-border scope and extensive historical AML context. This assessment does not determine fraud, suspicious activity, money laundering or a mandatory operational outcome." + }, + "missing_data": [ + { + "field": "customer_due_diligence_status", + "required_for_rule": "AML-GLOBAL-CDD-001", + "reason": "The input does not establish identity verification or completion of CDD." + }, + { + "field": "beneficial_owner_information", + "required_for_rule": "AML-GLOBAL-CDD-001 and AML-IL-001", + "reason": "No beneficial-owner or controlling-shareholder information is supplied." + }, + { + "field": "purpose_and_intended_nature", + "required_for_rule": "AML-GLOBAL-CDD-001 and AML-BD-001", + "reason": "The transaction purpose and intended nature are not provided." + }, + { + "field": "originator_address_or_identification_data", + "required_for_rule": "AML-CROSS-BORDER-WIRE-001", + "reason": "The retrieved cross-border wire rule requires address, national ID, or date and place of birth information." + }, + { + "field": "beneficiary_address_or_identification_data", + "required_for_rule": "AML-CROSS-BORDER-WIRE-001", + "reason": "The input does not provide the additional beneficiary information required by the retrieved wire-transfer policy." + }, + { + "field": "sanctions_screening_result", + "required_for_rule": "AML-GLOBAL-EDD-001", + "reason": "No sanctions-screening result is supplied." + }, + { + "field": "source_of_funds_and_source_of_wealth", + "required_for_rule": "AML-GLOBAL-CDD-001 and AML-GLOBAL-EDD-001", + "reason": "No source-of-funds or source-of-wealth evidence is supplied." + }, + { + "field": "bangladesh_authorised_purpose_code_and_supporting_documentation", + "required_for_rule": "AML-BD-001", + "reason": "The Bangladesh policy requires purpose and supporting documentation for applicable remittance activity." + }, + { + "field": "suspicious_activity_reporting_assessment", + "required_for_rule": "AML-IL-001 and global reporting controls", + "reason": "The input does not state whether the institution performed or documented a reporting assessment." + } + ], + "downstream_inputs": { + "has_regulatory_non_compliance": false, + "has_potential_regulatory_gap": true, + "requires_case_recommendation_review": true, + "highest_historical_context_level": 5, + "rules_triggered": [ + "AML-GLOBAL-CDD-001", + "AML-GLOBAL-TRM-001", + "AML-CROSS-BORDER-WIRE-001", + "AML-IL-001", + "AML-BD-001", + "AML-GLOBAL-EDD-001" + ], + "countries_evaluated": [ + "Israel", + "Bangladesh" + ], + "policy_scopes_evaluated": [ + "GLOBAL", + "SUPRANATIONAL", + "ORIGIN_COUNTRY", + "DESTINATION_COUNTRY", + "CROSS_BORDER", + "HISTORICAL_AML_CONTEXT" + ] + } + } +} +``` + +Confirm that the new alert appears on the Alert Management dashboard: + +![Alert Management dashboard showing the new alert](images/alert-management-dashboard.png) + +You can view the alert details or remove the alert to avoid duplicates in later tests. + + +### 6. Add the Alert Manager Agent to the Orchestration + +The final task is to add `AlertManagerAgent` to the orchestration so it can process transactions that require operational alerting. + +As in [Challenge 4](challenge-04.md), update the orchestration to include and connect the new agent. + +The source code is under the `walkthrough/challenge-05/orchestration` directory. The relevant files are: + +- `src/main.py`: Initializes and coordinates the agents in the orchestration. +- `azure.yaml`: Defines the agent for deployment as a **Hosted Agent** in **Microsoft Foundry**. + +Review the addition of the Alert Manager Agent: + +```python + alert_manager_agent = get_foundry_agent( + project_endpoint=project_endpoint, + credential=credential, + agent_name="AlertManagerAgent", + agent_version="7", + ) +``` + +The workflow then runs the AML Report Agent and Alert Manager Agent as parallel branches after regulatory assessment: + +```python + workflow_agent = ( + WorkflowBuilder( + start_executor=evidence_enrichment_executor, + # Limiting the output to only the final formatted result. + # If this is not set, all intermediate results will be included in the output. + output_from=[aml_report_executor], + ) + .add_edge(evidence_enrichment_executor, regulatory_assessment_executor) + .add_edge(regulatory_assessment_executor, aml_report_executor) + .add_edge(regulatory_assessment_executor, alert_manager_executor) + .build() + .as_agent() + ) +``` + +> **Important:** Review your agent versions in case you need to update them before deployment. + +Deploy the updated orchestration. + +> **Important:** Because of current source-code handling limitations in the extension, copy the orchestration source and `azure.yaml` to the project root before deployment: + +```bash +cp -Rf \ + "$walkthroughHome/challenge-05/orchestration/src" \ + "$walkthroughHome/challenge-05/orchestration/azure.yaml" \ + "$rootHome/" +``` + +For the complete **Foundry Toolkit** deployment procedure, refer to [Challenge 4](challenge-04.md). For this deployment: + +- Keep **Deployment method** set to **Code** and **Package mode** set to **Remote**. +- Set **Deploy to** to **Existing agent** because this deployment updates the orchestration with `AlertManagerAgent`. + +You can now test the full orchestration through the **Foundry Toolkit** or directly in **Microsoft Foundry**. + +Inspect the traces to verify that the orchestration invokes the new agent and that the alert appears in the dashboard. If you retained the alert from the standalone agent test, a repeated invocation may create a duplicate. + +Use these additional JSON payloads to test the orchestration: + +- A transaction with no fraudulent activity in the database: + +```json +{ + "transaction_id": "TX-TEST-0002", + "originator_name": "Oliver Thompson", + "origin_account": "844C89040", + "bank_origin": "3160527", + "beneficiary_name": "Sophie Williams", + "destination_account": "842700DC0", + "bank_destination": "237304", + "amount": 75000, + "currency": "EUR" +} +``` + +- A transaction that remains within the EU and has no evidence: + +```json +{ + "transaction_id": "TX-TEST-0003", + "originator_name": "Daniel Harris", + "origin_account": "80870EE60", + "bank_origin": "220048", + "beneficiary_name": "Charlotte Evans", + "destination_account": "8099ADB50", + "bank_destination": "31585", + "amount": 5000, + "currency": "EUR" +} +``` + +Finally, inspect the AI Gateway telemetry and agent traces to verify that model calls and both MCP integrations traverse the gateway. Confirm that logs do not expose credentials or unnecessary financial payloads. Where possible, send an unauthorized request and verify that the gateway rejects it. + +## 🚀 Go Further + +Add per-agent quotas and compare their effects under a short concurrent workload. Define a retry policy that respects API throttling and does not create duplicate alerts. + +## 🛠️ Troubleshooting + +- **Gateway requests are unauthorized:** Verify the caller identity, gateway audience or credential, role assignments, and backend authentication policy. +- **MCP tools are missing:** Confirm that the API operation is included in the imported specification and supported by the generated MCP interface. +- **An MCP tool name exceeds 64 characters:** Use a shorter MCP server name or shorter OpenAPI `operationId`, recreate the MCP integration, and start a new agent conversation. +- **Evidence calls bypass the gateway:** Check the agent's active version and MCP endpoint configuration. +- **Duplicate alerts are created:** Remove test alerts between runs and ensure that retries use the same stable case key. +- **A parallel branch hides a failure:** Inspect the traces and require each branch to return an explicit status. + +## 🧠 Conclusion + +You have placed model and MCP traffic behind a governance boundary and added operational alerting in parallel with report generation. Continue to [Challenge 6](challenge-06.md) to add operational and business observability to the complete workflow. diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-06.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-06.md new file mode 100644 index 000000000..b9c284cb8 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/challenge-06.md @@ -0,0 +1,241 @@ +# Challenge 6 - Observe Fraud Intelligence + +[Previous challenge](challenge-05.md) | **[Home](../README.md)** | [Finish](finish.md) + +## 🎯 Objective + +Add end-to-end **OpenTelemetry Protocol (OTLP)** tracing and business metrics to the hosted fraud intelligence workflow, send telemetry to **Application Insights**, and visualize operational and business outcomes in **Azure Managed Grafana**. + +## 🧭 Context and Background + +So far, you have built a multi-agent workflow that enriches transactions, assesses regulatory requirements, generates reports, and creates alerts. However, you still lack deep visibility into how the workflow behaves from end to end. In this challenge, you will add that observability. + +Production observability must answer two kinds of questions: whether the system is healthy and whether it is delivering the intended business outcome. A shared correlation identifier connects one transaction to its workflow, agent, model, retrieval, MCP, report, and alert activity. + +```mermaid +flowchart LR + TX[Transaction] --> ORCH[Fraud Intelligence workflow] + ORCH --> AGENTS[Agents and tools] + ORCH --> OTLP[OTLP traces and metrics] + AGENTS --> OTLP + OTLP --> APP[Application Insights] + APP --> GRAFANA[Azure Managed Grafana] + GRAFANA --> TECH[Technical dashboard] + GRAFANA --> BIZ[Business dashboard] +``` + +Telemetry must not become a second copy of sensitive financial data. Capture identifiers, timings, statuses, counts, and bounded classifications; avoid names, account numbers, full prompts, evidence payloads, and secrets. + +## ✅ Tasks + +### 1. Explore default telemetry in Application Insights + +In Challenge 2, you connected **App Insights** to **Microsoft Foundry**, but you did not explore the **App Insights** dashboard to view the default telemetry. + +Open the **Monitor** tab for any agent to review its captured metrics. To access the underlying traces, logs, and custom telemetry, select **Open in Azure Monitor**: + +![Open in Azure Monitor](/challenges/images/open-in-azure-monitor.png) + +You should see a dashboard similar to the one below, showing the default telemetry captured by Application Insights: + +![Application Insights Dashboard](/challenges/images/agents-appins-dashboard.png) + +Explore the sections and panels to understand the default telemetry and how it reflects agent behavior. + +Next, inspect a complete orchestration trace to see how the components interact and how telemetry is correlated across the workflow. + +Select **View Traces with Agent Runs**, then choose a **Dependency**: + +![View Traces with Agent Runs](/challenges/images/view-traces-with-agent-runs.png) + +The detailed trace shows the selected dependency within its orchestration run, including its interactions with other components and the telemetry correlation across the workflow. + +To explore and filter all traces, open **Search** in the **Investigate** section of the left sidebar: + +![Search in Investigate section](/challenges/images/search-in-investigate-section.png) + +Select a trace to review its execution details, including spans, attributes, and related telemetry: + +![Trace Details](/challenges/images/trace-details.png) + +These insights are provided by Application Insights through the agents' default auto-instrumentation. + +Next, add custom telemetry to the orchestration to capture business-specific metrics and additional trace context in Application Insights. + +### 2. Review the custom telemetry implementation + +The updated orchestration adds executors after specific agent executions. These executors inspect the responses and record custom metrics. + +The new code is under `/walkthrough/challenge-06/orchestration`. +Review the main orchestration code to see where the custom telemetry executors run after specific agent executions. The following image highlights the main changes: + +![Main Orchestration Code with Custom Telemetry Executors](/challenges/images/main-orchestration-code-with-custom-telemetry-executors.png) + +The metric definitions and configuration are in `walkthrough/challenge-06/orchestration/src/business_metrics.py`. + +Review this code to understand how each custom metric is defined and recorded. + +### 3. Redeploy the orchestration and generate telemetry + +After reviewing the custom telemetry setup, redeploy the orchestration to generate telemetry. + +> **Important:** Review your agent versions in case you need to update them before redeployment. + + +As in previous deployments, copy `src` and `azure.yaml` to the project root before redeploying: + +```bash +cp -Rf \ + "$walkthroughHome/challenge-06/orchestration/src" \ + "$walkthroughHome/challenge-06/orchestration/azure.yaml" \ + "$rootHome/" +``` + +Then redeploy using the **Foundry Toolkit**. + +After deployment, run the Python test script to send randomized requests and generate telemetry in **Application Insights**. + +To run the script, use the following command: + +```bash +$walkthroughHome/challenge-06/orchestration/run-tests.sh \ + --cosmos-endpoint "$cosmosEndpoint" \ + --database "aml" \ + --project-endpoint "https://$foundryAccountName.services.ai.azure.com/api/projects/$foundryProjectName" \ + --agent-name "fraud-intelligence-orchestration" \ + --count 10 +``` + +The script sends the number of requests specified by the `--count` argument. + +### 4. Verify custom telemetry in Application Insights + +In **Application Insights**, verify that the custom telemetry is being captured. Open **Logs** under **Monitoring**, select the `customMetrics` table, and run a query to view the recorded metrics. + +![Custom Telemetry in Application Insights](/challenges/images/custom-telemetry-in-application-insights.png) + +You should see metrics similar to those shown below: + +![Custom Metrics in Application Insights](/challenges/images/custom-metrics-in-application-insights.png) + +You can also query specific custom metrics directly from **Logs**. Open the selector on the right, choose **KQL mode**, enter a query in the editor, and select **Run**: + +![Custom Metrics Query in Application Insights](/challenges/images/custom-metrics-query-in-application-insights.png) + + +The following example queries provide several views of the custom metrics: + +- Transactions Processed by Currency (Last 24 Hours) + +```kusto +customMetrics +| where timestamp > ago(24h) +| where name == "fraud.transactions.processed" +| extend Currency = tostring(customDimensions["currency"]) +| summarize Transactions = sum(valueSum) + by Currency, bin(timestamp, 15m) +| render timechart +``` +- Transactions Processed by Cross-Border and History Detected (Last 24 Hours) + +```kusto +customMetrics +| where timestamp > ago(24h) +| where name == "fraud.transactions.processed" +| extend + CrossBorder = tostring(customDimensions["cross_border"]), + HistoryDetected = tostring(customDimensions["history_detected"]) +| summarize Transactions = sum(valueSum) + by CrossBorder, HistoryDetected +| render columnchart +``` +- AML Historical Context Level by Account Role (Last 24 Hours) + +```kusto +customMetrics +| where timestamp > ago(24h) +| where name == "fraud.aml.historical_context_level" +| extend AccountRole = tostring(customDimensions["account_role"]) +| summarize + AverageLevel = sum(valueSum) / sum(valueCount), + MaximumLevel = max(valueMax) + by AccountRole, bin(timestamp, 1h) +| render timechart +``` + +- AML Typology Detected by Pattern Type (Last 7 Days) + +```kusto +customMetrics +| where timestamp > ago(7d) +| where name == "fraud.aml.typology_detected" +| extend PatternType = tostring(customDimensions["pattern_type"]) +| summarize Detections = sum(valueSum) by PatternType +| order by Detections desc +| render piechart +``` + +The following image shows the result of the last query: + +![Result of AML Typology Detected by Pattern Type Query](/challenges/images/result-of-aml-typology-detected-by-pattern-type-query.png) + +Explore additional queries and visualizations as needed. + +Next, create **Graphical Dashboards** to visualize these custom metrics interactively. + +### 5. Import the Grafana dashboard + +Use Grafana to create interactive dashboards for the custom metrics queried in Application Insights. + +Return to the **Agents (Preview)** section introduced at the beginning of the lab, then select **Explore in Grafana** to open the Grafana integration: + +![Explore in Grafana](/challenges/images/explore-in-grafana.png) + +Grafana provides prebuilt dashboards and panels for visualizing the default telemetry: + +![Pre-built Dashboards in Grafana](/challenges/images/pre-built-dashboards-in-grafana.png) + +Explore these dashboards to become familiar with the interface. + +Next, create a custom dashboard for the metrics used in this workflow. The provided JSON file defines Grafana panels for the custom metrics. + +Select **Dashboards**, then **New**, and choose **Import**. Upload `walkthrough/challenge-06/grafana/fraud-intelligence-grafana-dashboard.json` to import the dashboard into Grafana. + +Then, use the following settings: + +- **Title**: FraudIntelligence +- **Subscription**: The one associated with your lab +- **Resource Group**: The one associated with your lab +- **Region**: swedencentral +- **Azure Monitor Datasource**: Azure Monitor + + +Select the correct **Application Insights** resource, whose name starts with `appi-fraud`. The imported dashboard should display panels similar to those below: + +![Custom metrics Grafana Dashboards](/challenges/images/custom-metrics-grafana-dashboards.png) + +You have now imported the dashboard and visualized the custom metrics in Grafana. + +### 6. Run the final validation + +Run additional requests and confirm that the custom metrics appear in both Application Insights and Grafana. Verify that transaction counts match the number of requests sent and that the dashboard distinguishes currencies, cross-border transactions, historical-context levels, account roles, and detected typologies. + +Inspect a complete orchestration trace and confirm that agent and dependency spans remain correlated. Ensure that no credentials, names, account numbers, full prompts, or evidence payloads appear in the custom telemetry. + +## 🚀 Go Further + +Create an Azure Monitor alert for sustained workflow failures or alert-delivery failures. Define a service-level objective for successful investigations and visualize its error budget. + +## 🛠️ Troubleshooting + +- **No telemetry appears:** Verify the exporter configuration, Application Insights connection, identity permissions, network access, and ingestion delay. +- **Spans are disconnected:** Confirm that trace context and the correlation identifier are propagated across async and parallel branches. +- **Metrics have unexpected counts:** Check retry behavior, metric emission points, and idempotency around completed state transitions. +- **Grafana cannot query Azure Monitor:** Verify the data source, managed identity, subscription access, and Grafana role assignments. +- **Queries are slow or expensive:** Reduce the time range, remove high-cardinality dimensions, and aggregate before visualization. +- **Sensitive data appears:** Stop emitting the attribute, deploy the correction, and follow the lab's process for handling previously ingested data. + +## 🧠 Conclusion + +You have instrumented the complete fraud intelligence workflow and created technical and business views of its behavior. Continue to the [finish page](finish.md) to complete the MicroHack. + diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/finish.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/finish.md new file mode 100644 index 000000000..2ec5aa031 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/finish.md @@ -0,0 +1,9 @@ +# Finish + +[Previous Challenge](challenge-06.md) - **[Home](../README.md)** + +## Congratulations + +You finished the Fraud Intelligence MicroHack. We hope you learned how to design and implement a production-ready multi-agent fraud intelligence solution. + +Thank you for investing the time, and see you next time! \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/add-azure-blob-storage-source.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/add-azure-blob-storage-source.png new file mode 100644 index 000000000..da2cc029f Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/add-azure-blob-storage-source.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/add-mcp-server-from-api.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/add-mcp-server-from-api.png new file mode 100644 index 000000000..a67d1b447 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/add-mcp-server-from-api.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/add-mcp-server-in-ai-gateway-portal.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/add-mcp-server-in-ai-gateway-portal.png new file mode 100644 index 000000000..cc49f6c89 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/add-mcp-server-in-ai-gateway-portal.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/add-rate-limiting-policy-for-admin-connected-model.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/add-rate-limiting-policy-for-admin-connected-model.png new file mode 100644 index 000000000..0f729460e Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/add-rate-limiting-policy-for-admin-connected-model.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/admin-connected-models-in-microsoft-foundry-after-adding.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/admin-connected-models-in-microsoft-foundry-after-adding.png new file mode 100644 index 000000000..6a2661e26 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/admin-connected-models-in-microsoft-foundry-after-adding.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/admin-connected-models-in-microsoft-foundry.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/admin-connected-models-in-microsoft-foundry.png new file mode 100644 index 000000000..97d678091 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/admin-connected-models-in-microsoft-foundry.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/agent-inspector.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/agent-inspector.png new file mode 100644 index 000000000..f0f48c571 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/agent-inspector.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/agents-appins-dashboard.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/agents-appins-dashboard.png new file mode 100644 index 000000000..16cc895d0 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/agents-appins-dashboard.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/agents-initialization.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/agents-initialization.png new file mode 100644 index 000000000..ab03704e9 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/agents-initialization.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/agents-versions.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/agents-versions.png new file mode 100644 index 000000000..a54bbbf6d Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/agents-versions.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/ai-gateway-deployment.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/ai-gateway-deployment.png new file mode 100644 index 000000000..63132ebc8 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/ai-gateway-deployment.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/alert-management-dashboard.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/alert-management-dashboard.png new file mode 100644 index 000000000..e5ba9a5d5 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/alert-management-dashboard.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/azure-extension-new-account.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/azure-extension-new-account.png new file mode 100644 index 000000000..fbbf48864 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/azure-extension-new-account.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/azureportal.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/azureportal.png new file mode 100644 index 000000000..5a5a5d966 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/azureportal.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/command-palette.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/command-palette.png new file mode 100644 index 000000000..2bb188c43 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/command-palette.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/configure-always-auto-approve-all-tools.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/configure-always-auto-approve-all-tools.png new file mode 100644 index 000000000..ff878bae6 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/configure-always-auto-approve-all-tools.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/configure-telemetry-in-ai-gateway-portal.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/configure-telemetry-in-ai-gateway-portal.png new file mode 100644 index 000000000..c9fff0e0d Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/configure-telemetry-in-ai-gateway-portal.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/connect-to-foundry-iq.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/connect-to-foundry-iq.png new file mode 100644 index 000000000..a05a0306f Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/connect-to-foundry-iq.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/connect-to-knowledge-base.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/connect-to-knowledge-base.png new file mode 100644 index 000000000..4e4462fb5 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/connect-to-knowledge-base.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/create-new-access-key.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/create-new-access-key.png new file mode 100644 index 000000000..c6489e4fd Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/create-new-access-key.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/custom-metrics-grafana-dashboards.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/custom-metrics-grafana-dashboards.png new file mode 100644 index 000000000..ad3c05ddb Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/custom-metrics-grafana-dashboards.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/custom-metrics-in-application-insights.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/custom-metrics-in-application-insights.png new file mode 100644 index 000000000..40cb09970 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/custom-metrics-in-application-insights.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/custom-metrics-query-in-application-insights.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/custom-metrics-query-in-application-insights.png new file mode 100644 index 000000000..c883f31b3 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/custom-metrics-query-in-application-insights.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/custom-telemetry-in-application-insights.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/custom-telemetry-in-application-insights.png new file mode 100644 index 000000000..5bc7915b4 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/custom-telemetry-in-application-insights.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/deploy-to-microsoft-foundry.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/deploy-to-microsoft-foundry.png new file mode 100644 index 000000000..20038aee9 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/deploy-to-microsoft-foundry.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/deployment-progress-in-output-panel.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/deployment-progress-in-output-panel.png new file mode 100644 index 000000000..b944c5794 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/deployment-progress-in-output-panel.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/detailed-execution-flow-of-last-trace.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/detailed-execution-flow-of-last-trace.png new file mode 100644 index 000000000..82b4dcf0d Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/detailed-execution-flow-of-last-trace.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/explore-in-grafana.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/explore-in-grafana.png new file mode 100644 index 000000000..20b21df96 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/explore-in-grafana.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/fin-evidence-mcp-key.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/fin-evidence-mcp-key.png new file mode 100644 index 000000000..18b20abc5 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/fin-evidence-mcp-key.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-auto-approve-tools.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-auto-approve-tools.png new file mode 100644 index 000000000..18228ee79 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-auto-approve-tools.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-blank-agent.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-blank-agent.png new file mode 100644 index 000000000..451963c27 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-blank-agent.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-configure-mcp-tool-menu.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-configure-mcp-tool-menu.png new file mode 100644 index 000000000..cf0c4d4f5 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-configure-mcp-tool-menu.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-configure-mcp-tool.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-configure-mcp-tool.png new file mode 100644 index 000000000..0e6870715 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-configure-mcp-tool.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-connected-resources.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-connected-resources.png new file mode 100644 index 000000000..c4b700091 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-connected-resources.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-add-sources.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-add-sources.png new file mode 100644 index 000000000..a9ac19112 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-add-sources.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-create-new-resource.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-create-new-resource.png new file mode 100644 index 000000000..f240b32e0 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-create-new-resource.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-global.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-global.png new file mode 100644 index 000000000..5023c096b Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-global.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-knowledge-base-creation.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-knowledge-base-creation.png new file mode 100644 index 000000000..46c53d846 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-knowledge-base-creation.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-knowledge.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-knowledge.png new file mode 100644 index 000000000..1574be5e9 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-knowledge.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-regional.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-regional.png new file mode 100644 index 000000000..61e112ea0 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-regional.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-traces.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-traces.png new file mode 100644 index 000000000..cc0b0e023 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-iq-traces.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-login.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-login.png new file mode 100644 index 000000000..0b5d4ed7c Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-login.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-new-agent.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-new-agent.png new file mode 100644 index 000000000..c5163b178 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-new-agent.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-test-agent.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-test-agent.png new file mode 100644 index 000000000..b08932444 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-test-agent.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-traces-evidence.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-traces-evidence.png new file mode 100644 index 000000000..5f867a557 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/foundry-traces-evidence.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/get-gateway-models-endpoint.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/get-gateway-models-endpoint.png new file mode 100644 index 000000000..b66d4c81d Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/get-gateway-models-endpoint.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/github-codespaces.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/github-codespaces.png new file mode 100644 index 000000000..d7512516f Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/github-codespaces.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/github-login.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/github-login.png new file mode 100644 index 000000000..6040440ee Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/github-login.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/github-organization.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/github-organization.png new file mode 100644 index 000000000..db7276bab Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/github-organization.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/github-repository.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/github-repository.png new file mode 100644 index 000000000..f5dc26875 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/github-repository.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/hosted-agent-playground.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/hosted-agent-playground.png new file mode 100644 index 000000000..864f54f2b Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/hosted-agent-playground.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/import-models-from-microsoft-foundry.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/import-models-from-microsoft-foundry.png new file mode 100644 index 000000000..9cb850618 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/import-models-from-microsoft-foundry.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/import-models-into-ai-gateway.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/import-models-into-ai-gateway.png new file mode 100644 index 000000000..b52cec900 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/import-models-into-ai-gateway.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/link-global-container.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/link-global-container.png new file mode 100644 index 000000000..9f080fb81 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/link-global-container.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/list-existing-alerts.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/list-existing-alerts.png new file mode 100644 index 000000000..c5b47460c Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/list-existing-alerts.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/list-of-current-alerts.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/list-of-current-alerts.png new file mode 100644 index 000000000..4255c41a6 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/list-of-current-alerts.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/list-tools-in-financial-evidence-mcp.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/list-tools-in-financial-evidence-mcp.png new file mode 100644 index 000000000..331b0b5be Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/list-tools-in-financial-evidence-mcp.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/main-orchestration-code-with-custom-telemetry-executors.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/main-orchestration-code-with-custom-telemetry-executors.png new file mode 100644 index 000000000..49a61085f Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/main-orchestration-code-with-custom-telemetry-executors.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/microsoft-foundry-hosted-agents.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/microsoft-foundry-hosted-agents.png new file mode 100644 index 000000000..2fde77000 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/microsoft-foundry-hosted-agents.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/models-listed-under-ai-gateway.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/models-listed-under-ai-gateway.png new file mode 100644 index 000000000..d8ef7708d Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/models-listed-under-ai-gateway.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/open-in-azure-monitor.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/open-in-azure-monitor.png new file mode 100644 index 000000000..730fe69d3 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/open-in-azure-monitor.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/orchestration-started.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/orchestration-started.png new file mode 100644 index 000000000..11309f1eb Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/orchestration-started.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/pre-built-dashboards-in-grafana.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/pre-built-dashboards-in-grafana.png new file mode 100644 index 000000000..772b26539 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/pre-built-dashboards-in-grafana.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/remove-existing-tool-from-evidenceenrichmentagent.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/remove-existing-tool-from-evidenceenrichmentagent.png new file mode 100644 index 000000000..e597de991 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/remove-existing-tool-from-evidenceenrichmentagent.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/resource-group-resources.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/resource-group-resources.png new file mode 100644 index 000000000..8361fdee3 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/resource-group-resources.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/resource-groups.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/resource-groups.png new file mode 100644 index 000000000..6b563f4c6 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/resource-groups.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/result-of-aml-typology-detected-by-pattern-type-query.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/result-of-aml-typology-detected-by-pattern-type-query.png new file mode 100644 index 000000000..5d36d59e2 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/result-of-aml-typology-detected-by-pattern-type-query.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/review-deployment-options.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/review-deployment-options.png new file mode 100644 index 000000000..9bf89fed7 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/review-deployment-options.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/run-get-bank-information-tool-in-financial-evidence-mcp.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/run-get-bank-information-tool-in-financial-evidence-mcp.png new file mode 100644 index 000000000..063cbb032 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/run-get-bank-information-tool-in-financial-evidence-mcp.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/search-in-investigate-section.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/search-in-investigate-section.png new file mode 100644 index 000000000..b20e9410e Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/search-in-investigate-section.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/select-admin-connected-model-for-agent.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/select-admin-connected-model-for-agent.png new file mode 100644 index 000000000..109b77568 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/select-admin-connected-model-for-agent.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/select-application-insights-instance-for-telemetry-in-ai-gateway-portal.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/select-application-insights-instance-for-telemetry-in-ai-gateway-portal.png new file mode 100644 index 000000000..d2c5295f3 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/select-application-insights-instance-for-telemetry-in-ai-gateway-portal.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/select-azure-subscription.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/select-azure-subscription.png new file mode 100644 index 000000000..627d6fbb2 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/select-azure-subscription.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/select-subscription-and-foundry-resource.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/select-subscription-and-foundry-resource.png new file mode 100644 index 000000000..d82b8954e Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/select-subscription-and-foundry-resource.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/select-token-rate-limit-policy-in-ai-gateway-portal.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/select-token-rate-limit-policy-in-ai-gateway-portal.png new file mode 100644 index 000000000..1953b807f Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/select-token-rate-limit-policy-in-ai-gateway-portal.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/set-rate-limit-parameters-in-ai-gateway-portal.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/set-rate-limit-parameters-in-ai-gateway-portal.png new file mode 100644 index 000000000..12904099f Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/set-rate-limit-parameters-in-ai-gateway-portal.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/storage-account-contents.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/storage-account-contents.png new file mode 100644 index 000000000..2294d4205 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/storage-account-contents.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/supported-backend-types-in-ai-gateway-portal.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/supported-backend-types-in-ai-gateway-portal.png new file mode 100644 index 000000000..a6ae8efa0 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/supported-backend-types-in-ai-gateway-portal.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/telemetry-graphs-in-ai-gateway-portal.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/telemetry-graphs-in-ai-gateway-portal.png new file mode 100644 index 000000000..864a3e8c8 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/telemetry-graphs-in-ai-gateway-portal.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/test-mcp-from-ai-gateway.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/test-mcp-from-ai-gateway.png new file mode 100644 index 000000000..4a41f6a77 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/test-mcp-from-ai-gateway.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/trace-details.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/trace-details.png new file mode 100644 index 000000000..eef38ebf8 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/trace-details.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/traces-in-agents-playground.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/traces-in-agents-playground.png new file mode 100644 index 000000000..165b113a1 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/traces-in-agents-playground.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/try-from-the-playground.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/try-from-the-playground.png new file mode 100644 index 000000000..081557efd Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/try-from-the-playground.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/use-mcp-in-agent.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/use-mcp-in-agent.png new file mode 100644 index 000000000..63d3f2229 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/use-mcp-in-agent.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/view-detailed-bank-information-in-financial-evidence-mcp.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/view-detailed-bank-information-in-financial-evidence-mcp.png new file mode 100644 index 000000000..cc0b68a77 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/view-detailed-bank-information-in-financial-evidence-mcp.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/view-traces-with-agent-runs.png b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/view-traces-with-agent-runs.png new file mode 100644 index 000000000..8982e3391 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/challenges/images/view-traces-with-agent-runs.png differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/labautomation/README.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/labautomation/README.md new file mode 100644 index 000000000..602fc5b2f --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/labautomation/README.md @@ -0,0 +1,25 @@ +# Lab automation + +This folder is optional. Delete it if this MicroHack does not need automated Azure provisioning. + +When present, the MicroHack platform reads [lab-defaults.json](lab-defaults.json) to determine how to scope lab environments. It invokes [deploy-lab.ps1](deploy-lab.ps1) once per participant and, when present, [shared-deploy-lab.ps1](shared-deploy-lab.ps1) once per subscription before participant deployments begin. + +## Folder layout + +```text +labautomation/ +|-- lab-defaults.json # Platform-facing configuration +|-- deploy-lab.ps1 # Per-participant deployment hook +|-- shared-deploy-lab.ps1 # Optional per-subscription deployment hook +`-- main.bicep # MicroHack-specific infrastructure +``` + +## Authoring guidance + +- Keep each participant deployment isolated. +- Put shared resources and one-time subscription preparation in `shared-deploy-lab.ps1`. +- Use `Get-MhhStableHash` when deterministic per-participant resource names are required. +- Return participant-facing values as `HackboxCredential` hashtables. +- Never return participant-specific secrets from the shared deployment hook. + +For the complete platform contract and helper cmdlet reference, see the [upstream MicroHack template documentation](https://github.com/microsoft/MicroHack/blob/main/99-MicroHack-Template/labautomation/README.md). \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/labautomation/deploy-lab.ps1 b/03-Azure/01-04-AI/07_Fraud_Intelligence/labautomation/deploy-lab.ps1 new file mode 100644 index 000000000..79deb441c --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/labautomation/deploy-lab.ps1 @@ -0,0 +1,88 @@ +<# +.SYNOPSIS +Deploys the lab resources scoped to a subscription or resource group. +.DESCRIPTION +Provides a controlled deployment flow for lab environments, optionally limited to a resource group and specific Entra user IDs. +.PARAMETER DeploymentType +Defines the deployment scope; allowed values are subscription, resourcegroup, or resourcegroup-with-subscriptionowner. +.PARAMETER SubscriptionId +Specifies the Azure subscription that contains the lab resources. +.PARAMETER ResourceGroupName +For resource group deployment, specifies the target resource group name. +.PARAMETER PreferredLocation +Specifies the preferred Azure regions, ordered by preference. +.PARAMETER AllowedEntraUserIds +Optional list of Entra user object IDs permitted to access the lab resources. +#> +param( + [Parameter(Mandatory=$true)] + [ValidateSet('subscription','resourcegroup', 'resourcegroup-with-subscriptionowner')] + [string]$DeploymentType, + + [Parameter(Mandatory=$true)] + [string]$SubscriptionId, + + [string]$ResourceGroupName = "", + + [string[]]$PreferredLocation = @(), + + [string[]]$AllowedEntraUserIds = @() +) + +$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition + +if($DeploymentType -eq 'resourcegroup' -and [string]::IsNullOrEmpty($ResourceGroupName)) { + throw "ResourceGroupName must be provided when DeploymentType is 'resourcegroup'." +} + +if($PreferredLocation.Count -gt 0) { + $effectiveLocation = $PreferredLocation[0] +} else { + $effectiveLocation = "swedencentral" +} + +if($DeploymentType -eq 'subscription') { + $stableHash = Get-MhhStablehash $AllowedEntraUserIds -Length 24 + $effectiveResourceGroup = "lab-$stableHash" + Write-Host "Deploying lab resources at the subscription level in subscription $SubscriptionId..." + if(-not (Get-AzResourceGroup -Name $effectiveResourceGroup -ErrorAction SilentlyContinue)) { + Write-Host "Creating resource group '$effectiveResourceGroup' in location '$effectiveLocation'..." + New-AzResourceGroup -Name $effectiveResourceGroup -Location $effectiveLocation -Verbose + } else { + Write-Host "Resource group '$effectiveResourceGroup' already exists." + } +} else { + $effectiveResourceGroup = $ResourceGroupName +} + +if($AllowedEntraUserIds.Count -eq 0) { + throw "At least one AllowedEntraUserIds value is required to assign lab access." +} + +$resourceSuffix = Get-MhhStablehash $AllowedEntraUserIds -Length 12 +$applicationInsightsName = "appi-fraud-$resourceSuffix" +$cosmosAccountName = "cosmosfraud$resourceSuffix" +$foundryAccountName = "aifraud$resourceSuffix" +$foundryProjectName = "fraud-intelligence" + +@{"HackboxCredential" = @{ name = "ResourceGroupName"; value = $effectiveResourceGroup; note = "The name of the resource group where lab resources are deployed" }} +@{"HackboxCredential" = @{ name = "ApplicationInsightsName"; value = $applicationInsightsName; note = "The name of the Application Insights resource" }} +@{"HackboxCredential" = @{ name = "CosmosAccountName"; value = $cosmosAccountName; note = "The name of the Azure Cosmos DB account" }} +@{"HackboxCredential" = @{ name = "FoundryAccountName"; value = $foundryAccountName; note = "The name of the Microsoft Foundry account" }} +@{"HackboxCredential" = @{ name = "FoundryProjectName"; value = $foundryProjectName; note = "The name of the Microsoft Foundry project" }} +@{"HackboxCredential" = @{ name = "EffectiveLocation"; value = $effectiveLocation; note = "The effective Azure region for the lab deployment" }} + +$template = Join-Path $scriptPath "main.bicep" +$templateParameters = @{ + location = $effectiveLocation + resourceSuffix = $resourceSuffix + userObjectId = $AllowedEntraUserIds[0] +} + +Invoke-MhhDeploymentWithRegionFallback ` + -PreferredLocations $PreferredLocation ` + -ResourceGroupName $effectiveResourceGroup ` + -RgOwnerEntraObjectIds $AllowedEntraUserIds ` + -Tag @{ CostControl = "Ignore"; SecurityControl = "Ignore" } ` + -TemplateFile $template ` + -TemplateParameterObject $templateParameters diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/labautomation/lab-defaults.json b/03-Azure/01-04-AI/07_Fraud_Intelligence/labautomation/lab-defaults.json new file mode 100644 index 000000000..b93d4e2c9 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/labautomation/lab-defaults.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/MicroHack/refs/heads/main/lab-defaults-schema.json", + "groups": ["GHCPUsers"], + "deploymentType": "resourcegroup-with-subscriptionowner", + "labsPerSubscription": 5, + "preferredLocation": "swedencentral", + "estimatedDailyCostsUsd": 5.0, + "estimatedSharedDeploymentDailyCostsUsd": 0.0 +} \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/labautomation/main.bicep b/03-Azure/01-04-AI/07_Fraud_Intelligence/labautomation/main.bicep new file mode 100644 index 000000000..5e516e4ae --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/labautomation/main.bicep @@ -0,0 +1,242 @@ +@description('Azure region used for all regional resources.') +param location string = resourceGroup().location + +@description('Stable suffix shared by globally unique lab resource names.') +@minLength(12) +@maxLength(12) +param resourceSuffix string + +@description('Microsoft Entra object ID of the participant who receives lab access.') +param userObjectId string + +var logAnalyticsName = 'log-fraud-${resourceSuffix}' +var applicationInsightsName = 'appi-fraud-${resourceSuffix}' +var containerAppsEnvironmentName = 'cae-fraud-${resourceSuffix}' +var foundryAccountName = 'aifraud${resourceSuffix}' +var foundryProjectName = 'fraud-intelligence' +var cosmosAccountName = 'cosmosfraud${resourceSuffix}' +var foundryUserRoleDefinitionId = '53ca6127-db72-4b80-b1b0-d745d6d5456d' +var cosmosDataContributorRoleDefinitionId = '00000000-0000-0000-0000-000000000002' + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: logAnalyticsName + location: location + properties: { + publicNetworkAccessForIngestion: 'Enabled' + publicNetworkAccessForQuery: 'Enabled' + retentionInDays: 30 + sku: { + name: 'PerGB2018' + } + } +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { + name: applicationInsightsName + location: location + kind: 'web' + properties: { + Application_Type: 'web' + Flow_Type: 'Bluefield' + Request_Source: 'rest' + WorkspaceResourceId: logAnalytics.id + publicNetworkAccessForIngestion: 'Enabled' + publicNetworkAccessForQuery: 'Enabled' + } +} + +resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2025-01-01' = { + name: containerAppsEnvironmentName + location: location + properties: { + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logAnalytics.properties.customerId + sharedKey: logAnalytics.listKeys().primarySharedKey + } + } + } +} + +resource alertManager 'Microsoft.App/containerApps@2025-01-01' = { + name: 'alert-manager' + location: location + properties: { + environmentId: containerAppsEnvironment.id + configuration: { + activeRevisionsMode: 'Single' + ingress: { + external: true + allowInsecure: false + targetPort: 8000 + transport: 'auto' + traffic: [ + { + latestRevision: true + weight: 100 + } + ] + } + } + template: { + containers: [ + { + name: 'alert-manager' + image: 'ghcr.io/dsanchor/fraud-alert-manager:sha-e7c4ecc' + resources: { + cpu: json('0.5') + memory: '1Gi' + } + } + ] + scale: { + minReplicas: 1 + maxReplicas: 1 + } + } + } +} + +resource foundryAccount 'Microsoft.CognitiveServices/accounts@2025-06-01' = { + name: foundryAccountName + location: location + kind: 'AIServices' + identity: { + type: 'SystemAssigned' + } + properties: { + allowProjectManagement: true + customSubDomainName: foundryAccountName + disableLocalAuth: false + publicNetworkAccess: 'Enabled' + restrictOutboundNetworkAccess: false + } + sku: { + name: 'S0' + } +} + +resource foundryProject 'Microsoft.CognitiveServices/accounts/projects@2025-06-01' = { + parent: foundryAccount + name: foundryProjectName + location: location + identity: { + type: 'SystemAssigned' + } + properties: { + description: 'Fraud Intelligence MicroHack project' + displayName: 'Fraud Intelligence' + } +} + +resource embeddingDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025-06-01' = { + parent: foundryAccount + name: 'text-embedding-3-large' + dependsOn: [ + chatDeployment + ] + properties: { + model: { + format: 'OpenAI' + name: 'text-embedding-3-large' + version: '1' + } + versionUpgradeOption: 'OnceNewDefaultVersionAvailable' + } + sku: { + name: 'GlobalStandard' + capacity: 100 + } +} + +resource chatDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025-06-01' = { + parent: foundryAccount + name: 'gpt-5.6-luna' + properties: { + model: { + format: 'OpenAI' + name: 'gpt-5.6-luna' + version: '2026-07-09' + } + versionUpgradeOption: 'OnceNewDefaultVersionAvailable' + } + sku: { + name: 'GlobalStandard' + capacity: 150 + } +} + +resource participantFoundryUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(foundryProject.id, userObjectId, foundryUserRoleDefinitionId) + scope: foundryProject + properties: { + principalId: userObjectId + principalType: 'User' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', foundryUserRoleDefinitionId) + } +} + +resource projectIdentityFoundryUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(foundryAccount.id, foundryProject.id, foundryUserRoleDefinitionId) + scope: foundryAccount + properties: { + principalId: foundryProject.identity.principalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', foundryUserRoleDefinitionId) + } +} + +resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2025-04-15' = { + name: cosmosAccountName + location: location + kind: 'GlobalDocumentDB' + properties: { + capabilities: [ + { + name: 'EnableServerless' + } + ] + consistencyPolicy: { + defaultConsistencyLevel: 'Session' + } + databaseAccountOfferType: 'Standard' + locations: [ + { + failoverPriority: 0 + isZoneRedundant: false + locationName: location + } + ] + minimalTlsVersion: 'Tls12' + publicNetworkAccess: 'Enabled' + } +} + +resource cosmosDatabase 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2025-04-15' = { + parent: cosmosAccount + name: 'aml' + properties: { + resource: { + id: 'aml' + } + } +} + +resource participantCosmosDataContributor 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2025-04-15' = { + parent: cosmosAccount + name: guid(cosmosAccount.id, userObjectId, cosmosDataContributorRoleDefinitionId) + properties: { + principalId: userObjectId + roleDefinitionId: '${cosmosAccount.id}/sqlRoleDefinitions/${cosmosDataContributorRoleDefinitionId}' + scope: cosmosAccount.id + } +} + +output applicationInsightsName string = applicationInsights.name +output applicationInsightsConnectionString string = applicationInsights.properties.ConnectionString +output alertManagerUrl string = 'https://${alertManager.properties.configuration.ingress.fqdn}' +output cosmosAccountName string = cosmosAccount.name +output cosmosEndpoint string = cosmosAccount.properties.documentEndpoint +output foundryAccountName string = foundryAccount.name +output foundryProjectName string = foundryProject.name \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/labautomation/shared-deploy-lab.ps1 b/03-Azure/01-04-AI/07_Fraud_Intelligence/labautomation/shared-deploy-lab.ps1 new file mode 100644 index 000000000..be4f926aa --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/labautomation/shared-deploy-lab.ps1 @@ -0,0 +1,56 @@ +<# +.SYNOPSIS +Runs once per subscription before any deploy-lab.ps1 run starts. +.DESCRIPTION +Deploy shared resources or perform one-time subscription preparation here. If this hook fails, participant deployments do not start. +.PARAMETER SubscriptionId +Specifies the Azure subscription that contains the lab resources. +.PARAMETER PreferredLocation +Specifies the preferred Azure regions, ordered by preference. +.PARAMETER AllowedEntraUserIds +Entra user object IDs of every participant holding a lab in this subscription. +#> +param( + [Parameter(Mandatory=$true)] + [string]$SubscriptionId, + + [Parameter(Mandatory=$true)] + [string[]]$PreferredLocation = @(), + + [Parameter(Mandatory=$false)] + [string[]]$AllowedEntraUserIds = @() +) + +$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition + +$requiredProviders = @( + "Microsoft.Monitor", + "Microsoft.DocumentDB", + "Microsoft.CognitiveServices", + "Microsoft.Search", + "Microsoft.App", + "Microsoft.ApiManagement", + "Microsoft.OperationalInsights", + "Microsoft.Insights", + "Microsoft.Storage" +) + +foreach($provider in $requiredProviders) { + $registrationState = (Get-AzResourceProvider ` + -ProviderNamespace $provider ` + -ErrorAction Stop | Select-Object -First 1).RegistrationState + + if($registrationState -eq "Registered") { + Write-Host "[$SubscriptionId] Resource provider $provider is already registered." + continue + } + + Write-Host "[$SubscriptionId] Registering resource provider: $provider" + Register-AzResourceProvider ` + -ProviderNamespace $provider ` + -ErrorAction Stop | Out-Null +} + +# Deploy shared resources here when needed. +# Any HackboxCredential emitted by this hook is visible to every participant +# sharing the subscription, so do not emit participant-specific secrets. \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-01/solution-01.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-01/solution-01.md new file mode 100644 index 000000000..186b6c9c5 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-01/solution-01.md @@ -0,0 +1,118 @@ +# Solution 1 - Prepare the Environment + +**[Home](../README.md)** | [Next solution](solution-02.md) + +## 🎯 Objective + +Sign in with your assigned lab credentials, verify the pre-provisioned Azure resources, and start the GitHub Codespaces development environment used throughout the Fraud Intelligence MicroHack. + +## 🧭 Context and Background + +The remaining challenges use a pre-provisioned Azure environment and a repository hosted on GitHub. You will work from a GitHub Codespace configured with the tools and dependencies required to build the fraud intelligence solution. + +Complete this setup with the credentials provided in your Hackbox. Use the assigned lab account rather than a personal or work account so that you have access to the correct Azure subscription, resource group, GitHub organization, and repository. + +## ✅ Tasks + +### 1. Sign in to Azure + +Open the [Azure portal](https://portal.azure.com) and sign in with the credentials provided in your Hackbox. + +When prompted to choose an account, select **Use another account** and enter the credentials provided. Do not use your personal or work account. + +![Sign in to the Azure portal with another account](/challenges/images/azureportal.png) + +In the Azure portal, select **Resource groups** from the navigation menu. + +![Resource groups in the Azure portal navigation menu](/challenges/images/resource-groups.png) + +Open the resource group assigned to you and confirm that its resources have been deployed successfully. + +![Resources deployed in the assigned resource group](/challenges/images/resource-group-resources.png) + +Verify that you can access the resources used in the later challenges, including the Microsoft Foundry project, model deployments, Azure Cosmos DB account, and Application Insights resource. + +### 2. Sign in to GitHub + +Open [GitHub](https://github.com) and sign in with the credentials provided in your Hackbox. + +When prompted to choose an account, select **Use another account**. Do not use your personal or work account. + +![Sign in to GitHub with another account](/challenges/images/github-login.png) + +Select **Sign in with your identity provider**, then use the assigned lab account to authenticate. + +Open the GitHub organization assigned to your lab account. + +![GitHub organization selector](/challenges/images/github-organization.png) + +Select the assigned organization, then open the `microhack` repository. Confirm that you can see the repository files and folders. + +![Files and folders in the GitHub repository](/challenges/images/github-repository.png) + +### 3. Create the development environment + +From the repository page, select **Code**, then open the **Codespaces** tab. Select the `...` menu and choose **New with options**. + +![Create a GitHub Codespace with options](/challenges/images/github-codespaces.png) + +For **Dev container configuration**, select **Azure / AI / Fraud Intelligence**, then select **Create codespace**. + +GitHub opens the Codespace in a new browser tab. Wait for the container setup to finish, then confirm that the repository files are visible in the Explorer and that the integrated terminal opens without errors. + +Open a terminal in the Codespace (Terminal > New Terminal) and run the following command to verify that Azure CLI is installed: + +```bash +az --version +``` + +Then sign in to Azure from the Codespace terminal: + +```bash +az login +``` + +Next, initialize a local environment file with the following commands: + +```bash +# init RG with your Resource Group name provided in Hackbox +rg= +DEPLOYMENT=$(az deployment group list -g "$rg" \ + --query "sort_by([?starts_with(name, 'mhh')], &properties.timestamp)[-1].name" \ + -o tsv) + +az deployment group show -g "$rg" -n "$DEPLOYMENT" \ + --query properties.outputs -o json | +jq -r 'to_entries[] | + "\(.key)=\(.value.value | + (if type == "string" then . else tojson end) | @sh)"' \ + > hackenv +az deployment group show -g "$rg" -n "$DEPLOYMENT" \ + --query properties.parameters -o json | +jq -r 'to_entries[] + | select(.value.value != null) + | "\(.key)=\(.value.value | + (if type == "string" then . else tojson end) | @sh)"' \ +>> hackenv + +chmod 600 hackenv +echo "rg=$rg" >> hackenv +cat hackenv +source hackenv +source baseenv +``` + +## 🚀 Go Further + +Review the [solution architecture](../README.md#architecture) and identify where each deployed Azure resource is used in the five build challenges. + +## 🛠️ Troubleshooting + +- **The Azure resource group is missing:** Confirm that you signed in with the assigned lab account and selected the assigned subscription in the Azure portal. +- **The GitHub organization or repository is missing:** Sign out of GitHub, then sign in again through **Sign in with your identity provider** using the Hackbox credentials. +- **The dev container configuration is unavailable:** Confirm that you opened the assigned `microhack` repository before creating the Codespace. +- **The Codespace does not finish starting:** Review the creation log for the failing step, then rebuild the container or recreate the Codespace. + +## 🧠 Conclusion + +You have verified the Azure resources and opened the configured development environment. Continue to [Challenge 2](solution-02.md) to build the Evidence Enrichment Agent. \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/cosmos_exports/aml/dim-bank.jsonl.gz b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/cosmos_exports/aml/dim-bank.jsonl.gz new file mode 100644 index 000000000..452d6ac7a Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/cosmos_exports/aml/dim-bank.jsonl.gz differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/cosmos_exports/aml/fact-laundering-pattern-txn.jsonl.gz b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/cosmos_exports/aml/fact-laundering-pattern-txn.jsonl.gz new file mode 100644 index 000000000..951c259fb Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/cosmos_exports/aml/fact-laundering-pattern-txn.jsonl.gz differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/cosmos_exports/aml/manifest.json b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/cosmos_exports/aml/manifest.json new file mode 100644 index 000000000..3346a3970 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/cosmos_exports/aml/manifest.json @@ -0,0 +1,26 @@ +{ + "formatVersion": 1, + "format": "jsonl+gzip", + "database": "aml", + "exportedAt": "2026-08-20T15:42:20.446069Z", + "containers": [ + { + "name": "dim-bank", + "file": "dim-bank.jsonl.gz", + "partitionKeyPaths": [ + "/partition_key" + ], + "documents": 2578, + "sha256": "3d67c3f82ce7759f965bfb716a61afb475e974194ae5dadccf9334418a5c1659" + }, + { + "name": "fact-laundering-pattern-txn", + "file": "fact-laundering-pattern-txn.jsonl.gz", + "partitionKeyPaths": [ + "/partition_key" + ], + "documents": 22743, + "sha256": "e9c8c12b84ffad3b26a26ff953c5b9c7febaa97f9c5d6fdf98d6b710c7cba55f" + } + ] +} diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/load-cosmos.sh b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/load-cosmos.sh new file mode 100644 index 000000000..9082af743 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/load-cosmos.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +VENV_DIR="${HOME}/venvs/amlcosmos" + +if [[ -z "${COSMOS_ENDPOINT:-}" ]]; then + if [[ -z "${1:-}" ]]; then + echo "Usage: $0 " >&2 + echo "Alternatively, set COSMOS_ENDPOINT before running this script." >&2 + exit 1 + fi + export COSMOS_ENDPOINT="https://${1}.documents.azure.com:443/" +fi + +export COSMOS_DATABASE="${COSMOS_DATABASE:-aml}" + +python3 -m venv "$VENV_DIR" +source "$VENV_DIR/bin/activate" +trap 'deactivate 2>/dev/null || true' EXIT + +python -m pip install -r "$SCRIPT_DIR/src/requirements-cosmos.txt" + +python "$SCRIPT_DIR/src/cosmos_load.py" import \ + --endpoint "$COSMOS_ENDPOINT" \ + --database "$COSMOS_DATABASE" \ + --input "$SCRIPT_DIR/cosmos_exports/aml" \ + --create-containers + +deactivate +trap - EXIT \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/src/cosmos_load.py b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/src/cosmos_load.py new file mode 100644 index 000000000..3b97c7e32 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/src/cosmos_load.py @@ -0,0 +1,255 @@ +import argparse +import asyncio +from datetime import datetime, timezone +import gzip +import hashlib +import json +import logging +import os +from pathlib import Path +from typing import Any + +from azure.cosmos import PartitionKey +from azure.cosmos.aio import CosmosClient +from azure.identity.aio import DefaultAzureCredential + + +LOGGER = logging.getLogger("load_cosmos") +DEFAULT_CONTAINERS = ["dim-bank", "fact-laundering-pattern-txn"] +SYSTEM_FIELDS = {"_rid", "_self", "_etag", "_attachments", "_ts"} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Export and import Azure Cosmos DB containers as compressed JSON Lines." + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + export_parser = subparsers.add_parser("export", help="Export containers.") + add_connection_args(export_parser) + export_parser.add_argument( + "--output", + type=Path, + default=Path("data/cosmos_exports/aml"), + ) + export_parser.add_argument( + "--container", + action="append", + help="Container to export; repeat to select multiple. Defaults to the AML containers.", + ) + + import_parser = subparsers.add_parser("import", help="Import an export package.") + add_connection_args(import_parser) + import_parser.add_argument("--input", type=Path, required=True) + import_parser.add_argument( + "--container", + action="append", + help="Container to import; repeat to select multiple. Defaults to all in the manifest.", + ) + import_parser.add_argument("--batch-size", type=int, default=500) + import_parser.add_argument("--concurrency", type=int, default=32) + import_parser.add_argument( + "--create-containers", + action="store_true", + help="Create the database and missing containers using the manifest.", + ) + + args = parser.parse_args() + if not args.endpoint: + parser.error("set COSMOS_ENDPOINT or pass --endpoint") + if args.command == "import" and (args.batch_size < 1 or args.concurrency < 1): + parser.error("--batch-size and --concurrency must be positive") + return args + + +def add_connection_args(parser: argparse.ArgumentParser) -> None: + parser.add_argument("--endpoint", default=os.getenv("COSMOS_ENDPOINT")) + parser.add_argument("--database", default=os.getenv("COSMOS_DATABASE", "aml")) + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + for chunk in iter(lambda: source.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +async def export_container(database: Any, name: str, output_dir: Path) -> dict[str, Any]: + container = database.get_container_client(name) + properties = await container.read() + partition_key_paths = properties["partitionKey"]["paths"] + output_path = output_dir / f"{name}.jsonl.gz" + count = 0 + + with gzip.open(output_path, "wt", encoding="utf-8", newline="\n") as output_file: + async for item in container.query_items("SELECT * FROM c"): + document = { + key: value for key, value in item.items() if key not in SYSTEM_FIELDS + } + output_file.write( + json.dumps(document, ensure_ascii=False, separators=(",", ":")) + ) + output_file.write("\n") + count += 1 + if count % 1000 == 0: + LOGGER.info("%s: %d documents exported", name, count) + + LOGGER.info("%s complete: %d documents", name, count) + return { + "name": name, + "file": output_path.name, + "partitionKeyPaths": partition_key_paths, + "documents": count, + "sha256": sha256_file(output_path), + } + + +async def export_data(args: argparse.Namespace, database: Any) -> None: + args.output.mkdir(parents=True, exist_ok=True) + containers = args.container or DEFAULT_CONTAINERS + exported = [] + for name in containers: + exported.append(await export_container(database, name, args.output)) + + manifest = { + "formatVersion": 1, + "format": "jsonl+gzip", + "database": args.database, + "exportedAt": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), + "containers": exported, + } + manifest_path = args.output / "manifest.json" + manifest_path.write_text( + json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + LOGGER.info("Export package written to %s", args.output) + + +def read_manifest(input_dir: Path) -> dict[str, Any]: + manifest_path = input_dir / "manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + if manifest.get("formatVersion") != 1 or manifest.get("format") != "jsonl+gzip": + raise ValueError("Unsupported Cosmos export format") + return manifest + + +async def import_container( + database: Any, + input_dir: Path, + entry: dict[str, Any], + batch_size: int, + concurrency: int, + create_containers: bool, +) -> int: + name = entry["name"] + partition_key_paths = entry["partitionKeyPaths"] + if len(partition_key_paths) != 1: + raise ValueError(f"{name}: hierarchical partition keys are not supported") + + file_name = Path(entry["file"]) + if file_name.name != str(file_name): + raise ValueError(f"{name}: invalid export file path") + input_path = input_dir / file_name + actual_hash = sha256_file(input_path) + if actual_hash != entry["sha256"]: + raise ValueError(f"{name}: SHA-256 mismatch") + + if create_containers: + container = await database.create_container_if_not_exists( + id=name, + partition_key=PartitionKey(path=partition_key_paths[0]), + ) + else: + container = database.get_container_client(name) + + properties = await container.read() + actual_paths = properties["partitionKey"]["paths"] + if actual_paths != partition_key_paths: + raise ValueError( + f"{name}: destination partition key is {actual_paths}; expected {partition_key_paths}" + ) + + semaphore = asyncio.Semaphore(concurrency) + + async def upsert(document: dict[str, Any]) -> None: + async with semaphore: + await container.upsert_item(document) + + imported = 0 + batch: list[dict[str, Any]] = [] + with gzip.open(input_path, "rt", encoding="utf-8") as input_file: + for line in input_file: + if not line.strip(): + continue + document = json.loads(line) + if "id" not in document: + raise ValueError(f"{name}: document without id") + batch.append(document) + if len(batch) >= batch_size: + await asyncio.gather(*(upsert(document) for document in batch)) + imported += len(batch) + batch.clear() + LOGGER.info("%s: %d documents imported", name, imported) + + if batch: + await asyncio.gather(*(upsert(document) for document in batch)) + imported += len(batch) + + if imported != entry["documents"]: + raise ValueError( + f"{name}: imported {imported} documents; expected {entry['documents']}" + ) + LOGGER.info("%s complete: %d documents", name, imported) + return imported + + +async def import_data(args: argparse.Namespace, database: Any) -> None: + manifest = read_manifest(args.input) + selected = set(args.container) if args.container else None + entries = [ + entry + for entry in manifest["containers"] + if selected is None or entry["name"] in selected + ] + if selected is not None: + missing = selected - {entry["name"] for entry in entries} + if missing: + raise ValueError(f"Containers not found in manifest: {sorted(missing)}") + + for entry in entries: + await import_container( + database, + args.input, + entry, + args.batch_size, + args.concurrency, + args.create_containers, + ) + + +async def run(args: argparse.Namespace) -> None: + async with DefaultAzureCredential() as credential: + async with CosmosClient(args.endpoint, credential=credential) as client: + if args.command == "export": + database = client.get_database_client(args.database) + await export_data(args, database) + else: + if args.create_containers: + database = await client.create_database_if_not_exists( + id=args.database + ) + else: + database = client.get_database_client(args.database) + await import_data(args, database) + + +def main() -> None: + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") + logging.getLogger("azure").setLevel(logging.WARNING) + asyncio.run(run(parse_args())) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/src/requirements-cosmos.txt b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/src/requirements-cosmos.txt new file mode 100644 index 000000000..6ee6028bb --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/cosmos/src/requirements-cosmos.txt @@ -0,0 +1,4 @@ +azure-cosmos==4.16.3 +azure-identity==1.25.3 +aiohttp==3.14.3 +pyarrow==25.0.1 \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/evidence-enrichment-agent/instructions.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/evidence-enrichment-agent/instructions.md new file mode 100644 index 000000000..6e4cebbc4 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/evidence-enrichment-agent/instructions.md @@ -0,0 +1,452 @@ +## Purpose + +You are an AML Evidence Enrichment Agent. + +Your sole purpose is to retrieve historical financial evidence through the Financial Evidence MCP, interpret it, and enrich one original transaction. The MCP tools are your only evidence source. + +Never access databases, Fabric, other agents, knowledge bases, or independent data sources. Never fabricate missing evidence. You are an explainability layer, not a decision-making layer. + +--- + +## Input + +You receive exactly one JSON transaction object: + +```json +{ + "transaction_id": "TX-TEST-0001", + "originator_name": "James Carter", + "origin_account": "83D4B1F30", + "bank_origin": "0121", + "beneficiary_name": "Emily Foster", + "destination_account": "818CCA030", + "bank_destination": "29196", + "amount": 15000, + "currency": "EUR" +} +``` + +Required fields are `transaction_id`, `originator_name`, `origin_account`, `bank_origin`, `beneficiary_name`, `destination_account`, `bank_destination`, `amount`, and `currency`. + +Bank and account identifiers are strings. Preserve leading zeroes. Do not normalize, rename, remove, or alter input fields. + +--- + +## MCP Response Envelope + +Every MCP tool returns a JSON-encoded string. Parse it as JSON before using it. + +Success: + +```json +{"ok": true, "data": {}} +``` + +Failure: + +```json +{"ok": false, "error": "Error description"} +``` + +Treat invalid JSON, `ok: false`, a missing `data` field, or invocation failure as an MCP error. A successful lookup with `found: false` or `count: 0` is valid evidence, not an error. + +--- + +## Available MCP Tools + +### `get_bank_information` - `[mcpToolTrigger]` + +Returns bank metadata for an exact bank identifier. + +Input: + +```json +{"bank_id": "0121"} +``` + +Successful `data`: + +```json +{ + "found": true, + "bank": { + "bank_id": "121", + "bank_name": "Example Bank", + "country": "Spain", + "id": "121", + "partition_key": "121" + } +} +``` + +When no bank exists, `found` is `false` and `bank` is `null`. + +### `list_account_transactions` - `[mcpToolTrigger]` + +Returns transactions where the exact bank/account pair appears as either the originator or destination. Results are ordered newest first. + +Input: + +```json +{ + "bank_id": "0121", + "account_id": "83D4B1F30", + "start_time": "", + "end_time": "", + "limit": 200 +} +``` + +`start_time` is optional and inclusive. `end_time` is optional and exclusive. Both use ISO 8601 timestamps. `limit` must be from 1 to 200. + +Successful `data`: + +```json +{ + "count": 1, + "transactions": [ + { + "pattern_txn_sk": 111669149723, + "attempt_id": 2220, + "pattern_type": "CYCLE", + "step_in_attempt": 8, + "date_key": 20260916, + "txn_ts": "2026-09-16T13:48:00Z", + "from_bank_id": "0046617", + "from_account": "8217EF2F0", + "to_bank_id": "012425", + "to_account": "80B69D680", + "amount_received": 12933.18, + "receiving_currency": "Euro", + "amount_paid": 12933.18, + "payment_currency": "Euro", + "payment_format": "ACH", + "is_laundering": 1, + "id": "111669149723", + "partition_key": "2220" + } + ] +} +``` + +### `get_transaction` - `[mcpToolTrigger]` + +Returns one historical transaction by Cosmos document `id` or numeric `pattern_txn_sk`. + +Input: + +```json +{"transaction_id": "111669149723"} +``` + +Successful `data` is `{"found": true, "transaction": {...}}`. The transaction has the fields shown above. When no transaction exists, `found` is `false` and `transaction` is `null`. + +Do not call this tool with the original business `transaction_id` unless it is known to be a historical Cosmos transaction identifier. Use it only to retrieve a known historical record that is not already complete in another response. + +### `get_laundering_attempt` - `[mcpToolTrigger]` + +Returns the ordered transaction timeline for one laundering attempt. + +Input: + +```json +{"attempt_id": 2220, "limit": 200} +``` + +Successful `data` is `{"attempt_id": 2220, "count": 2, "transactions": [...]}`. Transactions have the fields shown above and are ordered by `step_in_attempt` ascending. + +--- + +## Required Orchestration Workflow + +Follow these steps in order: + +1. Validate that the input is one JSON object containing every required field. +2. Preserve the complete input unchanged as `original_transaction`. +3. Invoke `get_bank_information` with `bank_origin`. +4. Invoke `get_bank_information` with `bank_destination`. If both bank IDs are identical, reuse the first successful response. +5. Invoke `list_account_transactions` with `bank_origin`, `origin_account`, and `limit: 200`. +6. Invoke `list_account_transactions` with `bank_destination`, `destination_account`, and `limit: 200`. If both bank/account pairs are identical, reuse the first successful response. +7. Use only records where `is_laundering` equals `1` to derive AML history. +8. Identify distinct `attempt_id` values in the laundering records. Invoke `get_laundering_attempt` only when a complete attempt timeline is needed to explain the evidence, and at most once per distinct attempt. +9. Invoke `get_transaction` only when a known historical record must be fetched by ID and is not already complete in another MCP response. +10. Calculate enrichments exclusively from successful MCP responses. +11. Convert the MCP evidence into the existing `data_agent_response` compatibility field. For each account, copy every record where `is_laundering` equals `1` into that account's `evidence` array without removing any MCP transaction field. Do not expose an `mcp_evidence` field or change the output schema. +12. Return exactly one enriched JSON object. + +Do not repeat equivalent tool calls. Do not use the original transaction's names, amount, or currency as historical evidence. + +The two `evidence` arrays are the compatibility location for transaction records returned by `list_account_transactions`. Never replace them with counts or summaries. Use an empty array only when that account has no laundering records. + +--- + +## Account Evidence Calculations + +Calculate each account independently from records where `is_laundering = 1`: + +- `historical_participation`: `true` if at least one laundering record exists; otherwise `false`. +- `laundering_attempt_count`: number of distinct non-null `attempt_id` values. +- `pattern_types`: distinct non-null `pattern_type` values, sorted alphabetically. +- `first_seen`: earliest `txn_ts`, or `null`. +- `last_seen`: latest `txn_ts`, or `null`. +- `role`: `Originator` when the exact bank/account pair appears only in `from_bank_id` plus `from_account`; `Receiver` when it appears only in `to_bank_id` plus `to_account`; `Both` when both roles occur; otherwise `null`. + +Use exact string equality when matching bank and account identifiers. + +--- + +## Historical Context Classification + +Classify each account independently. When multiple rules match, assign the highest matching level. + +| Level | Category | Rule | +| --- | --- | --- | +| 0 | No Historical AML Participation | `historical_participation = false` | +| 1 | Isolated Historical Participation | `laundering_attempt_count = 1` | +| 2 | Repeated Historical Participation | `laundering_attempt_count >= 2` and `pattern_diversity_count < 3` | +| 3 | Repeated Recent Participation | `laundering_attempt_count >= 2` and `days_since_last_seen <= 30` | +| 4 | Repeated Participation Across Multiple Patterns | `laundering_attempt_count >= 2` and `pattern_diversity_count >= 3` | +| 5 | Extensive Historical Participation | `laundering_attempt_count >= 4` and `pattern_diversity_count >= 3` | + +Calculate `days_since_last_seen` as the number of complete UTC calendar days between `last_seen` and the current date. If `last_seen` is missing, return `null`. + +--- + +## Derived Indicators + +### Participation Consistency + +| Historical role | Participation consistency | +| --- | --- | +| `Originator` | `Originator Only` | +| `Receiver` | `Receiver Only` | +| `Both` | `Mixed Participation` | +| `null` | `None` | + +### Pattern Diversity + +Set `pattern_diversity_count` to the number of distinct values in `pattern_types`. + +| Count | Diversity level | +| --- | --- | +| 0 | `None` | +| 1 | `Low` | +| 2 | `Medium` | +| 3 or more | `High` | + +### Participation Frequency + +| Laundering attempts | Frequency | +| --- | --- | +| 0 | `None` | +| 1 | `Isolated` | +| 2-3 | `Repeated` | +| 4 or more | `Extensive` | + +### Historical Recency + +| Days since last seen | Recency band | +| --- | --- | +| `null` | `None` | +| 0-30 | `Recent` | +| 31-180 | `Moderate` | +| More than 180 | `Historical` | + +--- + +## Investigator Interpretation + +Generate one short, factual explanation per account. It must remain descriptive, evidence-based, and consistent with the derived indicators. + +Examples: + +- `No historical AML participation was identified.` +- `The account participated in multiple confirmed AML records, consistently acting as a Receiver.` +- `The account appeared across several AML typologies and multiple independent laundering attempts.` +- `The account shows repeated historical participation spanning multiple laundering patterns.` + +Do not describe the current transaction as suspicious, assign risk, infer intent, or recommend an action. + +Generate exactly one transaction-level interpretation: + +- `No account involved in the transaction has historical AML participation.` +- `One account involved in the transaction has historical AML participation.` +- `Both accounts involved in the transaction have historical AML participation.` + +--- + +## Output Requirements + +Return exactly one valid JSON object and no other text. + +- Do not use Markdown or code fences in the response. +- Place the complete, unchanged input transaction in `original_transaction`. +- Place the MCP-derived compatibility response in `data_agent_response`. +- Do not add, remove, rename, or restructure any output field. +- Always return every enrichment field in the schema. +- Use `null` when a derived scalar cannot be calculated. +- Use only the allowed category values defined in these instructions. + +```json +{ + "transaction_id": "TX-TEST-0001", + "original_transaction": { + "transaction_id": "TX-TEST-0001", + "originator_name": "James Carter", + "origin_account": "83D4B1F30", + "bank_origin": "0121", + "beneficiary_name": "Emily Foster", + "destination_account": "818CCA030", + "bank_destination": "29196", + "amount": 15000, + "currency": "EUR" + }, + "data_agent_response": { + "transaction_id": "TX-TEST-0001", + "match_found": true, + "assessment": "Historical laundering records were found for one or more accounts in the transaction.", + "origin_account": { + "originator_name": "James Carter", + "account": "83D4B1F30", + "bank_id": "0121", + "bank_name": "Example Bank", + "country": "Spain", + "historical_participation": true, + "role": "Receiver", + "laundering_transaction_count": 1, + "laundering_attempt_count": 1, + "first_seen": "2026-09-16T13:48:00Z", + "last_seen": "2026-09-16T13:48:00Z", + "pattern_types": ["CYCLE"], + "evidence": [ + { + "pattern_txn_sk": 111669149723, + "attempt_id": 2220, + "pattern_type": "CYCLE", + "step_in_attempt": 8, + "date_key": 20260916, + "txn_ts": "2026-09-16T13:48:00Z", + "from_bank_id": "0046617", + "from_account": "8217EF2F0", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 12933.18, + "receiving_currency": "Euro", + "amount_paid": 12933.18, + "payment_currency": "Euro", + "payment_format": "ACH", + "is_laundering": 1, + "id": "111669149723", + "partition_key": "2220" + } + ] + }, + "destination_account": { + "beneficiary_name": "Emily Foster", + "account": "818CCA030", + "bank_id": "29196", + "bank_name": "Example Destination Bank", + "country": "Bangladesh", + "historical_participation": false, + "role": null, + "laundering_transaction_count": 0, + "laundering_attempt_count": 0, + "first_seen": null, + "last_seen": null, + "pattern_types": [], + "evidence": [] + } + }, + "historical_context": { + "accounts_with_history": 1, + "accounts_without_history": 1, + "highest_historical_context_level": 1, + "transaction_interpretation": "One account involved in the transaction has historical AML participation." + }, + "origin_account_enrichment": { + "historical_context_level": 1, + "historical_context_category": "Isolated Historical Participation", + "participation_consistency": "Receiver Only", + "participation_frequency": "Isolated", + "pattern_diversity_count": 1, + "pattern_diversity_level": "Low", + "days_since_last_seen": 12, + "recency_band": "Recent", + "investigator_interpretation": "The account participated in one confirmed AML record, acting as a Receiver." + }, + "destination_account_enrichment": { + "historical_context_level": 0, + "historical_context_category": "No Historical AML Participation", + "participation_consistency": "None", + "participation_frequency": "None", + "pattern_diversity_count": 0, + "pattern_diversity_level": "None", + "days_since_last_seen": null, + "recency_band": "None", + "investigator_interpretation": "No historical AML participation was identified." + }, + "derived_features": { + "origin_has_history": true, + "destination_has_history": false, + "multiple_pattern_presence": false, + "recent_historical_activity": true, + "highest_historical_context_level": 1 + }, + "aml_regulatory_inputs": { + "historical_context_level": 1, + "pattern_diversity_count": 1, + "laundering_attempt_count": 1, + "recent_participation": true, + "historical_participation_role": "Receiver", + "historical_participation_detected": true + } +} +``` + +Populate `data_agent_response` from MCP evidence while preserving this exact compatibility structure. Set `match_found` to `true` when either account has laundering history. Preserve `originator_name`, `beneficiary_name`, `account`, and the original input `bank_id` exactly. Populate `bank_name` and `country` from `get_bank_information`. Do not replace an input bank identifier with the normalized identifier returned by the bank dimension. + +For each account, set `laundering_transaction_count` to the length of `evidence`, and copy every matching MCP transaction where `is_laundering` equals `1` into `evidence`. Preserve all transaction fields and values exactly as returned by the MCP. Do not place raw MCP envelopes in the output and do not omit, summarize, truncate, or deduplicate distinct evidence records. + +Populate `aml_regulatory_inputs` with values from the account that has the highest historical context level. If both accounts have the same level, use the account with the greatest `laundering_attempt_count`; if still tied, use the origin account. This block provides normalized factual inputs only and must not contain a regulatory conclusion or risk assessment. + +--- + +### Data Agent Error Contract + +For downstream compatibility, if a required MCP invocation fails or returns invalid JSON, return the existing Data Agent error contract exactly as shown: + +```json +{ + "transaction_id": "TX-TEST-0001", + "original_transaction": { + "transaction_id": "TX-TEST-0001", + "originator_name": "James Carter", + "origin_account": "83D4B1F30", + "bank_origin": "0121", + "beneficiary_name": "Emily Foster", + "destination_account": "818CCA030", + "bank_destination": "29196", + "amount": 15000, + "currency": "EUR" + }, + "status": "data_agent_error", + "error": { + "code": "DATA_AGENT_INVALID_RESPONSE", + "message": "The AML Historical Evidence Data Agent did not return a valid response." + } +} +``` + +Do not include historical context, derived features, regulatory inputs, or partially retrieved evidence in an error response. + +--- + +## Forbidden Actions + +Never assign AML risk, generate fraud scores, recommend investigation, escalation or blocking, predict suspicious activity, infer intent, or perform regulatory evaluation. Those responsibilities belong to downstream agents. + +--- + +## Final Rule + +The original transaction is the orchestration input. The Financial Evidence MCP is the only historical evidence source. Retrieve both banks and both account histories, preserve every returned laundering record in the corresponding `data_agent_response` account's `evidence` array, derive factual historical indicators, and never perform risk assessment or regulatory decision-making. \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/deploy.sh b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/deploy.sh new file mode 100644 index 000000000..e41a0170c --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/deploy.sh @@ -0,0 +1,312 @@ +#!/usr/bin/env bash +# Deploy the Financial Evidence MCP to Azure Functions Flex Consumption. +# The Function uses system-assigned managed identity for host storage and +# read-only access to an existing Azure Cosmos DB for NoSQL database. +# +# Usage: +# ./deploy.sh -g -a -d [options] +# +# Required: +# -g Function App resource group +# -a Existing Cosmos DB account name +# -d Existing Cosmos DB database name +# +# Options: +# -c Cosmos DB resource group (defaults to the Function App resource group) +# -l Azure location (default: swedencentral) +# -n Function App name (generated when omitted) +# -s Function host storage account name (generated when omitted) +# -b Bank container name (default: dim-bank) +# -t Transaction container name (default: fact-laundering-pattern-txn) +# -u Azure subscription ID +# -h Show this help +# +# Requirements: Azure CLI, Azure Functions Core Tools, and permissions to +# create Function resources and Cosmos DB SQL role assignments. + +set -euo pipefail + +LOCATION="swedencentral" +FUNCTION_APP_NAME="" +STORAGE_NAME="" +RESOURCE_GROUP="" +COSMOS_RESOURCE_GROUP="" +COSMOS_ACCOUNT_NAME="" +COSMOS_DATABASE_NAME="" +BANK_CONTAINER_NAME="dim-bank" +TRANSACTION_CONTAINER_NAME="fact-laundering-pattern-txn" +SUBSCRIPTION="" +COSMOS_DATA_READER_ROLE_ID="00000000-0000-0000-0000-000000000001" + +usage() { + grep '^#' "$0" | sed 's/^# \{0,1\}//' + exit "${1:-0}" +} + +while getopts ":g:a:d:c:l:n:s:b:t:u:h" opt; do + case "$opt" in + g) RESOURCE_GROUP="$OPTARG" ;; + a) COSMOS_ACCOUNT_NAME="$OPTARG" ;; + d) COSMOS_DATABASE_NAME="$OPTARG" ;; + c) COSMOS_RESOURCE_GROUP="$OPTARG" ;; + l) LOCATION="$OPTARG" ;; + n) FUNCTION_APP_NAME="$OPTARG" ;; + s) STORAGE_NAME="$OPTARG" ;; + b) BANK_CONTAINER_NAME="$OPTARG" ;; + t) TRANSACTION_CONTAINER_NAME="$OPTARG" ;; + u) SUBSCRIPTION="$OPTARG" ;; + h) usage 0 ;; + :) echo "ERROR: -$OPTARG requires a value." >&2; usage 1 ;; + *) echo "ERROR: unknown option -$OPTARG." >&2; usage 1 ;; + esac +done + +if [[ -z "$RESOURCE_GROUP" || -z "$COSMOS_ACCOUNT_NAME" || -z "$COSMOS_DATABASE_NAME" ]]; then + echo "ERROR: -g, -a, and -d are required." >&2 + usage 1 +fi + +command -v az >/dev/null || { echo "ERROR: Azure CLI not found." >&2; exit 1; } +command -v func >/dev/null || { + echo "ERROR: Azure Functions Core Tools (func) not found." >&2 + exit 1 +} + +if [[ -n "$SUBSCRIPTION" ]]; then + az account set --subscription "$SUBSCRIPTION" +fi + +SUBSCRIPTION_ID="$(az account show --query id --output tsv)" +COSMOS_RESOURCE_GROUP="${COSMOS_RESOURCE_GROUP:-$RESOURCE_GROUP}" +SUFFIX="$(printf '%s' "${RESOURCE_GROUP}-${SUBSCRIPTION_ID}" | sha256sum | cut -c1-6)" +FUNCTION_APP_NAME="${FUNCTION_APP_NAME:-mcp-financial-evidence-${SUFFIX}}" +STORAGE_NAME="${STORAGE_NAME:-mcpfinance${SUFFIX}sa}" +STORAGE_NAME="$(printf '%s' "$STORAGE_NAME" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9' | cut -c1-24)" +DEPLOY_CONTAINER="app-package-${FUNCTION_APP_NAME}" + +if (( ${#STORAGE_NAME} < 3 )); then + echo "ERROR: the normalized storage account name must contain at least 3 characters." >&2 + exit 1 +fi + +echo "Subscription : $SUBSCRIPTION_ID" +echo "Function RG : $RESOURCE_GROUP" +echo "Location : $LOCATION" +echo "Function App : $FUNCTION_APP_NAME (Flex Consumption, Python 3.13)" +echo "Storage account : $STORAGE_NAME" +echo "Cosmos account : $COSMOS_ACCOUNT_NAME" +echo "Cosmos database : $COSMOS_DATABASE_NAME" +echo "Cosmos resource RG : $COSMOS_RESOURCE_GROUP" + +if ! az group show --name "$RESOURCE_GROUP" >/dev/null 2>&1; then + echo ">>> Creating resource group $RESOURCE_GROUP" + az group create --name "$RESOURCE_GROUP" --location "$LOCATION" --output none +fi + +if [[ "$(az provider show \ + --namespace Microsoft.Web \ + --query registrationState --output tsv)" != "Registered" ]]; then + echo ">>> Registering Microsoft.Web resource provider" + az provider register \ + --namespace Microsoft.Web \ + --wait \ + --output none +fi + +echo ">>> Validating Cosmos DB resources" +COSMOS_ACCOUNT_ID="$(az cosmosdb show \ + --resource-group "$COSMOS_RESOURCE_GROUP" \ + --name "$COSMOS_ACCOUNT_NAME" \ + --query id --output tsv)" +COSMOS_ENDPOINT="$(az cosmosdb show \ + --resource-group "$COSMOS_RESOURCE_GROUP" \ + --name "$COSMOS_ACCOUNT_NAME" \ + --query documentEndpoint --output tsv)" +az cosmosdb sql database show \ + --resource-group "$COSMOS_RESOURCE_GROUP" \ + --account-name "$COSMOS_ACCOUNT_NAME" \ + --name "$COSMOS_DATABASE_NAME" \ + --output none + +if ! az storage account show \ + --name "$STORAGE_NAME" \ + --resource-group "$RESOURCE_GROUP" >/dev/null 2>&1; then + echo ">>> Creating storage account $STORAGE_NAME" + az storage account create \ + --name "$STORAGE_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --location "$LOCATION" \ + --sku Standard_LRS \ + --kind StorageV2 \ + --allow-blob-public-access false \ + --public-network-access Enabled \ + --default-action Allow \ + --min-tls-version TLS1_2 \ + --output none +fi + +STORAGE_ID="$(az storage account show \ + --name "$STORAGE_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --query id --output tsv)" +CURRENT_USER_ID="$(az ad signed-in-user show --query id --output tsv 2>/dev/null || true)" + +if [[ -n "$CURRENT_USER_ID" ]]; then + if [[ "$(az role assignment list \ + --assignee "$CURRENT_USER_ID" \ + --role "Storage Blob Data Owner" \ + --scope "$STORAGE_ID" \ + --query 'length(@)' --output tsv)" == "0" ]]; then + echo ">>> Granting the current user deployment storage access" + az role assignment create \ + --assignee-object-id "$CURRENT_USER_ID" \ + --assignee-principal-type User \ + --role "Storage Blob Data Owner" \ + --scope "$STORAGE_ID" \ + --output none + fi +else + echo "ERROR: unable to resolve the signed-in user for deployment storage access." >&2 + echo "Run this script from an interactive Azure CLI user session." >&2 + exit 1 +fi + +echo ">>> Ensuring deployment container $DEPLOY_CONTAINER" +container_created=false +for attempt in 1 2 3 4 5 6; do + if az storage container create \ + --name "$DEPLOY_CONTAINER" \ + --account-name "$STORAGE_NAME" \ + --auth-mode login \ + --output none 2>/dev/null; then + container_created=true + break + fi + echo " Storage RBAC is propagating (attempt $attempt/6)..." + sleep 5 +done +if [[ "$container_created" != "true" ]]; then + echo "ERROR: could not create deployment container after waiting for RBAC." >&2 + exit 1 +fi + +if ! az functionapp show \ + --name "$FUNCTION_APP_NAME" \ + --resource-group "$RESOURCE_GROUP" >/dev/null 2>&1; then + echo ">>> Creating Function App $FUNCTION_APP_NAME" + az functionapp create \ + --resource-group "$RESOURCE_GROUP" \ + --name "$FUNCTION_APP_NAME" \ + --flexconsumption-location "$LOCATION" \ + --runtime python \ + --runtime-version 3.13 \ + --storage-account "$STORAGE_NAME" \ + --deployment-storage-name "$STORAGE_NAME" \ + --deployment-storage-container-name "$DEPLOY_CONTAINER" \ + --deployment-storage-auth-type SystemAssignedIdentity \ + --assign-identity '[system]' \ + --output none +else + echo ">>> Ensuring the existing Function App has a system-assigned identity" + az functionapp identity assign \ + --name "$FUNCTION_APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --output none +fi + +echo ">>> Configuring one always-ready HTTP instance" +az functionapp scale config always-ready set \ + --resource-group "$RESOURCE_GROUP" \ + --name "$FUNCTION_APP_NAME" \ + --settings http=1 \ + --output none + +FUNCTION_PRINCIPAL_ID="$(az functionapp identity show \ + --name "$FUNCTION_APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --query principalId --output tsv)" + +ensure_storage_role() { + local role="$1" + if [[ "$(az role assignment list \ + --assignee "$FUNCTION_PRINCIPAL_ID" \ + --role "$role" \ + --scope "$STORAGE_ID" \ + --query 'length(@)' --output tsv)" == "0" ]]; then + az role assignment create \ + --assignee-object-id "$FUNCTION_PRINCIPAL_ID" \ + --assignee-principal-type ServicePrincipal \ + --role "$role" \ + --scope "$STORAGE_ID" \ + --output none + fi +} + +echo ">>> Granting Function host storage roles" +ensure_storage_role "Storage Blob Data Owner" +ensure_storage_role "Storage Queue Data Contributor" +ensure_storage_role "Storage Table Data Contributor" + +COSMOS_SCOPE="${COSMOS_ACCOUNT_ID}/dbs/${COSMOS_DATABASE_NAME}" +COSMOS_ROLE_DEFINITION_ID="${COSMOS_ACCOUNT_ID}/sqlRoleDefinitions/${COSMOS_DATA_READER_ROLE_ID}" +EXISTING_COSMOS_ASSIGNMENT="$(az cosmosdb sql role assignment list \ + --resource-group "$COSMOS_RESOURCE_GROUP" \ + --account-name "$COSMOS_ACCOUNT_NAME" \ + --query "[?principalId=='${FUNCTION_PRINCIPAL_ID}' && scope=='${COSMOS_SCOPE}' && roleDefinitionId=='${COSMOS_ROLE_DEFINITION_ID}'] | length(@)" \ + --output tsv)" + +if [[ "$EXISTING_COSMOS_ASSIGNMENT" == "0" ]]; then + echo ">>> Granting Cosmos DB Built-in Data Reader at database scope" + az cosmosdb sql role assignment create \ + --resource-group "$COSMOS_RESOURCE_GROUP" \ + --account-name "$COSMOS_ACCOUNT_NAME" \ + --principal-id "$FUNCTION_PRINCIPAL_ID" \ + --role-definition-id "$COSMOS_ROLE_DEFINITION_ID" \ + --scope "$COSMOS_SCOPE" \ + --output none +else + echo ">>> Cosmos DB data reader assignment already exists" +fi + +echo ">>> Configuring identity-based storage and Cosmos settings" +az functionapp config appsettings delete \ + --resource-group "$RESOURCE_GROUP" \ + --name "$FUNCTION_APP_NAME" \ + --setting-names AzureWebJobsStorage COSMOS_CONNECTION_STRING \ + --output none 2>/dev/null || true +az functionapp config appsettings set \ + --resource-group "$RESOURCE_GROUP" \ + --name "$FUNCTION_APP_NAME" \ + --settings \ + "AzureWebJobsStorage__accountName=$STORAGE_NAME" \ + "AzureWebJobsFeatureFlags=EnableWorkerIndexing" \ + "COSMOS_ENDPOINT=$COSMOS_ENDPOINT" \ + "COSMOS_DATABASE_NAME=$COSMOS_DATABASE_NAME" \ + "COSMOS_BANK_CONTAINER=$BANK_CONTAINER_NAME" \ + "COSMOS_TRANSACTION_CONTAINER=$TRANSACTION_CONTAINER_NAME" \ + --output none + +echo ">>> Publishing Function code" +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/src" && pwd)" +if [[ ! -f "$PROJECT_DIR/host.json" ]]; then + echo "ERROR: Azure Functions project not found at $PROJECT_DIR." >&2 + exit 1 +fi +pushd "$PROJECT_DIR" >/dev/null +func azure functionapp publish "$FUNCTION_APP_NAME" --python +popd >/dev/null + +HOSTNAME="${FUNCTION_APP_NAME}.azurewebsites.net" +DEPLOYED_HOSTNAME="$(az rest \ + --method get \ + --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.Web/sites/${FUNCTION_APP_NAME}?api-version=2024-11-01" \ + --query properties.defaultHostName \ + --output tsv 2>/dev/null || true)" +if [[ -n "$DEPLOYED_HOSTNAME" ]]; then + HOSTNAME="$DEPLOYED_HOSTNAME" +fi + +echo +echo "Done." +echo "Function App: https://${HOSTNAME}" +echo "MCP endpoint: https://${HOSTNAME}/runtime/webhooks/mcp" \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/.funcignore b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/.funcignore new file mode 100644 index 000000000..525cb75cc --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/.funcignore @@ -0,0 +1,8 @@ +.git* +.venv/ +tests/ +local.settings.json +local.settings.json.example +requirements-dev.txt +__pycache__/ +.pytest_cache/ \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/.gitignore b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/.gitignore new file mode 100644 index 000000000..9dbb40dfb --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/.gitignore @@ -0,0 +1,5 @@ +local.settings.json +.azurite/ +__pycache__/ +.pytest_cache/ +.venv/ \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/README.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/README.md new file mode 100644 index 000000000..1466e9da6 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/README.md @@ -0,0 +1,87 @@ +# Financial Evidence MCP + +Read-only MCP tools hosted by the Python v2 Azure Functions runtime and backed +by Azure Cosmos DB for NoSQL. + +## Tools + +| Tool | Purpose | +|---|---| +| `get_bank_information` | Point-read a bank by `bank_id`. | +| `list_account_transactions` | Find transactions where a bank/account pair is the originator or destination. | +| `get_transaction` | Find one transaction by document id or transaction surrogate key. | +| `get_laundering_attempt` | Return the ordered steps for one laundering attempt. | + +Bank and account identifiers are strings. Do not coerce them to numbers because +leading zeroes are significant. + +## Configuration + +Create `local.settings.json` from `local.settings.json.example`. The service +uses these settings: + +| Setting | Required | Description | +|---|---|---| +| `COSMOS_ENDPOINT` | Yes in Azure | Cosmos DB account endpoint used with `DefaultAzureCredential`. | +| `COSMOS_CONNECTION_STRING` | Local alternative | Optional local-only connection string. Do not set this in the Function App. | +| `COSMOS_DATABASE_NAME` | Yes | Database containing the evidence containers. | +| `COSMOS_BANK_CONTAINER` | No | Defaults to `dim-bank`. | +| `COSMOS_TRANSACTION_CONTAINER` | No | Defaults to `fact-laundering-pattern-txn`. | + +Assign the Function App's managed identity the **Cosmos DB Built-in Data +Reader** data-plane role on the account or database. The identity also needs +permission to read Cosmos account metadata. + +## Run locally + +Prerequisites are Python 3.13 or newer, Azure Functions Core Tools 4.8 or newer, +Azurite, and an Azure identity with Cosmos read access. + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -r requirements-dev.txt +cp local.settings.json.example local.settings.json +az login +azurite --silent --location .azurite & +func start +``` + +The MCP endpoint is `http://localhost:7071/runtime/webhooks/mcp`. + +## Deploy + +The deployment script creates a Python 3.13 Flex Consumption Function App and +host storage, enables system-assigned managed identity, and publishes the code. +It grants the identity **Cosmos DB Built-in Data Reader** on the specified +existing database; it does not retrieve or store Cosmos keys. + +```bash +./deploy.sh \ + -g \ + -a \ + -d +``` + +Use `-c ` when Cosmos DB is in a different resource +group. Run `./deploy.sh -h` for naming, location, subscription, and container +options. The caller needs permission to create Function resources, assign Azure +storage roles, and create Cosmos DB SQL data-plane role assignments. + +For a remote endpoint, enable App Service Authentication with Microsoft Entra ID +or place the endpoint behind API Management. The MCP extension's webhook is +anonymous so the platform authentication layer must enforce remote access. + +## Cosmos indexing + +The account transaction tool performs a bounded cross-partition query. Keep +range indexes for these transaction paths: + +- `/from_bank_id/?`, `/from_account/?` +- `/to_bank_id/?`, `/to_account/?` +- `/txn_ts/?`, `/id/?` +- `/attempt_id/?`, `/step_in_attempt/?` + +For production-scale evidence, consider a materialized account-transaction +container partitioned by a normalized `bank_id|account_id` key. That avoids the +cross-partition `OR` query while preserving the source transaction container. \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/cosmos_repository.py b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/cosmos_repository.py new file mode 100644 index 000000000..f7608c2a7 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/cosmos_repository.py @@ -0,0 +1,269 @@ +import logging +import os +from datetime import datetime, timezone +from itertools import islice +from typing import Any, Iterable + +from azure.cosmos import CosmosClient +from azure.cosmos.exceptions import CosmosResourceNotFoundError +from azure.identity import DefaultAzureCredential + + +logger = logging.getLogger(__name__) + +_BANK_FIELDS = ("bank_id", "bank_name", "country", "id", "partition_key") +_TRANSACTION_FIELDS = ( + "pattern_txn_sk", + "attempt_id", + "pattern_type", + "step_in_attempt", + "date_key", + "txn_ts", + "from_bank_id", + "from_account", + "to_bank_id", + "to_account", + "amount_received", + "receiving_currency", + "amount_paid", + "payment_currency", + "payment_format", + "is_laundering", + "id", + "partition_key", +) + + +class CosmosRepository: + def __init__(self, bank_container: Any, transaction_container: Any) -> None: + self._bank_container = bank_container + self._transaction_container = transaction_container + + @classmethod + def from_environment(cls) -> "CosmosRepository": + database_name = _required_environment("COSMOS_DATABASE_NAME") + bank_container_name = os.getenv("COSMOS_BANK_CONTAINER", "dim-bank") + transaction_container_name = os.getenv( + "COSMOS_TRANSACTION_CONTAINER", "fact-laundering-pattern-txn" + ) + + connection_string = os.getenv("COSMOS_CONNECTION_STRING") + if connection_string: + logger.info( + "Creating Cosmos repository: database=%s auth=connection_string", + database_name, + ) + client = CosmosClient.from_connection_string(connection_string) + else: + endpoint = _required_environment("COSMOS_ENDPOINT") + logger.info( + "Creating Cosmos repository: database=%s auth=managed_identity", + database_name, + ) + client = CosmosClient(endpoint, credential=DefaultAzureCredential()) + + database = client.get_database_client(database_name) + logger.info( + "Cosmos repository ready: bank_container=%s transaction_container=%s", + bank_container_name, + transaction_container_name, + ) + return cls( + database.get_container_client(bank_container_name), + database.get_container_client(transaction_container_name), + ) + + def get_bank(self, bank_id: str) -> dict[str, Any] | None: + normalized_bank_id = _required_string(bank_id, "bank_id").lstrip("0") or "0" + logger.info( + "Cosmos get_bank started: bank_id=%s normalized_bank_id=%s", + bank_id, + normalized_bank_id, + ) + try: + item = self._bank_container.read_item( + item=normalized_bank_id, + partition_key=normalized_bank_id, + ) + except CosmosResourceNotFoundError: + logger.info("Cosmos get_bank completed: bank_id=%s found=false", bank_id) + return None + logger.info("Cosmos get_bank completed: bank_id=%s found=true", bank_id) + return _select_fields(item, _BANK_FIELDS) + + def list_account_transactions( + self, + bank_id: str, + account_id: str, + start_time: str | None = None, + end_time: str | None = None, + limit: int = 50, + ) -> list[dict[str, Any]]: + normalized_bank_id = _required_string(bank_id, "bank_id") + normalized_account_id = _required_string(account_id, "account_id") + normalized_limit = _bounded_limit(limit) + logger.info( + "Cosmos list_account_transactions started: bank_id=%s account_id=%s " + "start_time=%s end_time=%s limit=%s", + normalized_bank_id, + normalized_account_id, + start_time, + end_time, + normalized_limit, + ) + + predicates = [ + "((c.from_bank_id = @bank_id AND c.from_account = @account_id)", + "OR (c.to_bank_id = @bank_id AND c.to_account = @account_id))", + ] + parameters: list[dict[str, Any]] = [ + {"name": "@limit", "value": normalized_limit}, + {"name": "@bank_id", "value": normalized_bank_id}, + {"name": "@account_id", "value": normalized_account_id}, + ] + + if start_time: + predicates.append("AND c.txn_ts >= @start_time") + parameters.append( + {"name": "@start_time", "value": _iso_timestamp(start_time, "start_time")} + ) + if end_time: + predicates.append("AND c.txn_ts < @end_time") + parameters.append( + {"name": "@end_time", "value": _iso_timestamp(end_time, "end_time")} + ) + + query = ( + "SELECT TOP @limit * FROM c WHERE " + + " ".join(predicates) + + " ORDER BY c.txn_ts DESC" + ) + items = self._transaction_container.query_items( + query=query, + parameters=parameters, + enable_cross_partition_query=True, + max_item_count=normalized_limit, + ) + transactions = _transactions(items, normalized_limit) + logger.info( + "Cosmos list_account_transactions completed: bank_id=%s account_id=%s count=%s", + normalized_bank_id, + normalized_account_id, + len(transactions), + ) + return transactions + + def get_transaction(self, transaction_id: str) -> dict[str, Any] | None: + normalized_id = _required_string(transaction_id, "transaction_id") + logger.info("Cosmos get_transaction started: transaction_id=%s", normalized_id) + predicates = ["c.id = @transaction_id"] + parameters: list[dict[str, Any]] = [ + {"name": "@transaction_id", "value": normalized_id} + ] + try: + pattern_txn_sk = int(normalized_id) + except ValueError: + pass + else: + predicates.append("c.pattern_txn_sk = @pattern_txn_sk") + parameters.append({"name": "@pattern_txn_sk", "value": pattern_txn_sk}) + + items = self._transaction_container.query_items( + query="SELECT TOP 1 * FROM c WHERE " + " OR ".join(predicates), + parameters=parameters, + enable_cross_partition_query=True, + max_item_count=1, + ) + transactions = _transactions(items, 1) + transaction = transactions[0] if transactions else None + logger.info( + "Cosmos get_transaction completed: transaction_id=%s found=%s", + normalized_id, + str(transaction is not None).lower(), + ) + return transaction + + def get_laundering_attempt( + self, attempt_id: int, limit: int = 100 + ) -> list[dict[str, Any]]: + try: + normalized_attempt_id = int(attempt_id) + except (TypeError, ValueError) as exc: + raise ValueError("attempt_id must be an integer") from exc + if normalized_attempt_id < 0: + raise ValueError("attempt_id must be zero or greater") + + normalized_limit = _bounded_limit(limit) + partition_key = str(normalized_attempt_id) + logger.info( + "Cosmos get_laundering_attempt started: attempt_id=%s limit=%s", + normalized_attempt_id, + normalized_limit, + ) + items = self._transaction_container.query_items( + query=( + "SELECT TOP @limit * FROM c " + "WHERE c.attempt_id = @attempt_id " + "ORDER BY c.step_in_attempt ASC" + ), + parameters=[ + {"name": "@limit", "value": normalized_limit}, + {"name": "@attempt_id", "value": normalized_attempt_id}, + ], + partition_key=partition_key, + max_item_count=normalized_limit, + ) + transactions = _transactions(items, normalized_limit) + logger.info( + "Cosmos get_laundering_attempt completed: attempt_id=%s count=%s", + normalized_attempt_id, + len(transactions), + ) + return transactions + + +def _required_environment(name: str) -> str: + value = os.getenv(name, "").strip() + if not value: + raise ValueError(f"Required setting {name} is not configured") + return value + + +def _required_string(value: Any, name: str) -> str: + normalized = str(value).strip() if value is not None else "" + if not normalized: + raise ValueError(f"{name} is required") + return normalized + + +def _bounded_limit(value: Any) -> int: + try: + limit = int(value) + except (TypeError, ValueError) as exc: + raise ValueError("limit must be an integer from 1 to 200") from exc + if not 1 <= limit <= 200: + raise ValueError("limit must be an integer from 1 to 200") + return limit + + +def _iso_timestamp(value: str, name: str) -> str: + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError as exc: + raise ValueError(f"{name} must be an ISO 8601 timestamp") from exc + if parsed.tzinfo is None: + parsed = parsed.replace(tzinfo=timezone.utc) + return parsed.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +def _select_fields(item: dict[str, Any], fields: tuple[str, ...]) -> dict[str, Any]: + return {field: item[field] for field in fields if field in item} + + +def _transactions( + items: Iterable[dict[str, Any]], limit: int +) -> list[dict[str, Any]]: + return [ + _select_fields(item, _TRANSACTION_FIELDS) + for item in islice(items, limit) + ] \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/function_app.py b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/function_app.py new file mode 100644 index 000000000..d6f4238db --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/function_app.py @@ -0,0 +1,139 @@ +import json +import logging +from functools import lru_cache +from typing import Any, Callable + +import azure.functions as func + +from cosmos_repository import CosmosRepository + + +app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) +logger = logging.getLogger(__name__) + + +@lru_cache(maxsize=1) +def _repository() -> CosmosRepository: + logger.info("Initializing Cosmos repository") + return CosmosRepository.from_environment() + + +def _response(operation_name: str, operation: Callable[[], Any]) -> str: + logger.info("MCP operation started: %s", operation_name) + try: + result = operation() + logger.info("MCP operation completed: %s", operation_name) + return json.dumps({"ok": True, "data": result}, default=str) + except ValueError as exc: + logger.warning("MCP operation rejected: %s error=%s", operation_name, exc) + return json.dumps({"ok": False, "error": str(exc)}) + except Exception: + logger.exception("MCP operation failed: %s", operation_name) + return json.dumps( + {"ok": False, "error": "The financial evidence service is unavailable."} + ) + + +@app.mcp_tool() +@app.mcp_tool_property( + arg_name="bank_id", + description="Bank identifier. Leading zeroes are normalized for the bank dimension.", + is_required=True, +) +def get_bank_information(bank_id: str) -> str: + """Get a bank's name and country from the bank dimension.""" + + def operation() -> dict[str, Any]: + bank = _repository().get_bank(bank_id) + return {"found": bank is not None, "bank": bank} + + return _response("get_bank_information", operation) + + +@app.mcp_tool() +@app.mcp_tool_property( + arg_name="bank_id", + description="Exact bank identifier. Pass it as a string to preserve leading zeroes.", + is_required=True, +) +@app.mcp_tool_property( + arg_name="account_id", + description="Exact account identifier at the bank.", + is_required=True, +) +@app.mcp_tool_property( + arg_name="start_time", + description="Optional inclusive ISO 8601 transaction timestamp.", + is_required=False, +) +@app.mcp_tool_property( + arg_name="end_time", + description="Optional exclusive ISO 8601 transaction timestamp.", + is_required=False, +) +@app.mcp_tool_property( + arg_name="limit", + description="Maximum transactions to return, from 1 to 200.", + is_required=False, +) +def list_account_transactions( + bank_id: str, + account_id: str, + start_time: str = "", + end_time: str = "", + limit: int = 50, +) -> str: + """List transactions where the bank and account are either party.""" + + def operation() -> dict[str, Any]: + transactions = _repository().list_account_transactions( + bank_id=bank_id, + account_id=account_id, + start_time=start_time or None, + end_time=end_time or None, + limit=limit, + ) + return {"count": len(transactions), "transactions": transactions} + + return _response("list_account_transactions", operation) + + +@app.mcp_tool() +@app.mcp_tool_property( + arg_name="transaction_id", + description="Transaction document id or pattern transaction surrogate key.", + is_required=True, +) +def get_transaction(transaction_id: str) -> str: + """Get one transaction by its identifier.""" + + def operation() -> dict[str, Any]: + transaction = _repository().get_transaction(transaction_id) + return {"found": transaction is not None, "transaction": transaction} + + return _response("get_transaction", operation) + + +@app.mcp_tool() +@app.mcp_tool_property( + arg_name="attempt_id", + description="Money-laundering pattern attempt identifier.", + is_required=True, +) +@app.mcp_tool_property( + arg_name="limit", + description="Maximum timeline steps to return, from 1 to 200.", + is_required=False, +) +def get_laundering_attempt(attempt_id: int, limit: int = 100) -> str: + """Get an ordered transaction timeline for a laundering attempt.""" + + def operation() -> dict[str, Any]: + transactions = _repository().get_laundering_attempt(attempt_id, limit) + return { + "attempt_id": attempt_id, + "count": len(transactions), + "transactions": transactions, + } + + return _response("get_laundering_attempt", operation) \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/host.json b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/host.json new file mode 100644 index 000000000..2276c4116 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/host.json @@ -0,0 +1,23 @@ +{ + "version": "2.0", + "extensions": { + "mcp": { + "system": { + "webhookAuthorizationLevel": "Anonymous" + } + } + }, + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle.Preview", + "version": "[4.*, 5.0.0)" + } +} \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/local.settings.json.example b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/local.settings.json.example new file mode 100644 index 000000000..a8bd8bf97 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/local.settings.json.example @@ -0,0 +1,11 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "python", + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "COSMOS_ENDPOINT": "https://.documents.azure.com:443/", + "COSMOS_DATABASE_NAME": "", + "COSMOS_BANK_CONTAINER": "dim-bank", + "COSMOS_TRANSACTION_CONTAINER": "fact-laundering-pattern-txn" + } +} \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/requirements-dev.txt b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/requirements-dev.txt new file mode 100644 index 000000000..0dc6ff2bb --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/requirements-dev.txt @@ -0,0 +1,2 @@ +-r requirements.txt +pytest>=8.3.0,<9.0.0 \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/requirements.txt b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/requirements.txt new file mode 100644 index 000000000..ac6eb214f --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/requirements.txt @@ -0,0 +1,5 @@ +# The Azure Functions platform manages azure-functions-worker. +azure-functions>=2.2.0b3 +azure-cosmos>=4.9.0,<5.0.0 +azure-identity>=1.19.0,<2.0.0 +mcp>=1.12.0,<2.0.0 \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/tests/test_cosmos_repository.py b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/tests/test_cosmos_repository.py new file mode 100644 index 000000000..8d0ba0c2b --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/tests/test_cosmos_repository.py @@ -0,0 +1,113 @@ +import logging + +from cosmos_repository import CosmosRepository + + +class FakeContainer: + def __init__(self, items=None): + self.items = items or [] + self.read_arguments = None + self.query_arguments = None + + def read_item(self, **kwargs): + self.read_arguments = kwargs + return self.items[0] + + def query_items(self, **kwargs): + self.query_arguments = kwargs + return iter(self.items) + + +def test_get_bank_normalizes_leading_zeroes_for_point_read(caplog): + banks = FakeContainer( + [ + { + "id": "121", + "partition_key": "121", + "bank_id": "121", + "bank_name": "Bank #121", + "country": "United States", + "_etag": "secret-metadata", + } + ] + ) + repository = CosmosRepository(banks, FakeContainer()) + + with caplog.at_level(logging.INFO, logger="cosmos_repository"): + bank = repository.get_bank("0121") + + assert banks.read_arguments == {"item": "121", "partition_key": "121"} + assert bank == { + "bank_id": "121", + "bank_name": "Bank #121", + "country": "United States", + "id": "121", + "partition_key": "121", + } + assert "Cosmos get_bank started: bank_id=0121 normalized_bank_id=121" in caplog.text + assert "Cosmos get_bank completed: bank_id=0121 found=true" in caplog.text + + +def test_account_query_matches_originator_and_destination_with_time_bounds(): + transactions = FakeContainer([{"id": "1", "_rid": "metadata"}]) + repository = CosmosRepository(FakeContainer(), transactions) + + result = repository.list_account_transactions( + bank_id="0046617", + account_id="8217EF2F0", + start_time="2026-09-01T00:00:00Z", + end_time="2026-10-01T00:00:00Z", + limit=25, + ) + + query = transactions.query_arguments["query"] + assert "c.from_bank_id = @bank_id AND c.from_account = @account_id" in query + assert "c.to_bank_id = @bank_id AND c.to_account = @account_id" in query + assert "c.txn_ts >= @start_time" in query + assert "c.txn_ts < @end_time" in query + assert transactions.query_arguments["enable_cross_partition_query"] is True + assert {item["name"]: item["value"] for item in transactions.query_arguments["parameters"]} == { + "@limit": 25, + "@bank_id": "0046617", + "@account_id": "8217EF2F0", + "@start_time": "2026-09-01T00:00:00Z", + "@end_time": "2026-10-01T00:00:00Z", + } + assert result == [{"id": "1"}] + + +def test_attempt_query_targets_its_partition_and_orders_steps(): + transactions = FakeContainer([{"id": "2", "step_in_attempt": 1}]) + repository = CosmosRepository(FakeContainer(), transactions) + + result = repository.get_laundering_attempt(2220, limit=100) + + assert transactions.query_arguments["partition_key"] == "2220" + assert "ORDER BY c.step_in_attempt ASC" in transactions.query_arguments["query"] + assert result == [{"step_in_attempt": 1, "id": "2"}] + + +def test_transaction_lookup_uses_document_and_numeric_surrogate_keys(): + transactions = FakeContainer([{"id": "111669149723"}]) + repository = CosmosRepository(FakeContainer(), transactions) + + result = repository.get_transaction("111669149723") + + assert "c.id = @transaction_id" in transactions.query_arguments["query"] + assert "c.pattern_txn_sk = @pattern_txn_sk" in transactions.query_arguments["query"] + assert transactions.query_arguments["parameters"] == [ + {"name": "@transaction_id", "value": "111669149723"}, + {"name": "@pattern_txn_sk", "value": 111669149723}, + ] + assert result == {"id": "111669149723"} + + +def test_limit_is_bounded(): + repository = CosmosRepository(FakeContainer(), FakeContainer()) + + try: + repository.list_account_transactions("bank", "account", limit=201) + except ValueError as exc: + assert str(exc) == "limit must be an integer from 1 to 200" + else: + raise AssertionError("Expected an invalid limit to fail") \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/tests/test_function_app.py b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/tests/test_function_app.py new file mode 100644 index 000000000..0e1013e48 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/financial-evidence-mcp/src/tests/test_function_app.py @@ -0,0 +1,24 @@ +import json +import logging + +from function_app import _response + + +def test_response_logs_operation_start_and_completion(caplog): + with caplog.at_level(logging.INFO, logger="function_app"): + response = _response("test_operation", lambda: {"found": True}) + + assert json.loads(response) == {"ok": True, "data": {"found": True}} + assert "MCP operation started: test_operation" in caplog.text + assert "MCP operation completed: test_operation" in caplog.text + + +def test_response_logs_validation_errors(caplog): + def invalid_operation(): + raise ValueError("invalid input") + + with caplog.at_level(logging.INFO, logger="function_app"): + response = _response("test_operation", invalid_operation) + + assert json.loads(response) == {"ok": False, "error": "invalid input"} + assert "MCP operation rejected: test_operation error=invalid input" in caplog.text diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/solution-02.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/solution-02.md new file mode 100644 index 000000000..fae7e4c5c --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-02/solution-02.md @@ -0,0 +1,235 @@ +# Challenge 2 - Build the Evidence Enrichment Agent + +[Previous challenge](solution-01.md) | **[Home](../README.md)** | [Next challenge](solution-03.md) + +## 🎯 Objective + +Load historical AML evidence into Azure Cosmos DB, deploy a read-only **Financial Evidence MCP**, and connect it to an **Evidence Enrichment Agent** in Microsoft Foundry. + +## 🧭 Context and Background + +The Evidence Enrichment Agent receives a transaction and retrieves relevant banks, accounts, historical transactions, and laundering evidence. It adds traceable facts to the original transaction without making a compliance decision. + +```mermaid +flowchart LR + TX[Transaction JSON] --> AGENT[Evidence Enrichment Agent] + AGENT -->|Calls evidence tools| MCP[Financial Evidence MCP] + MCP -->|Reads evidence| COSMOS[(Azure Cosmos DB)] + COSMOS -->|Returns financial records| MCP + MCP -->|Returns tool results| AGENT + AGENT --> RESULT[Enriched transaction] +``` + +In this challenge, you will populate the Cosmos DB evidence store, review and deploy the MCP source code, connect Application Insights to your Foundry project, configure the agent, and inspect its tool calls in the Foundry playground. + +## ✅ Tasks + +### 1. Load the evidence data into Cosmos DB + +The Azure Cosmos DB account and `aml` database were provisioned in Challenge 1. Load the supplied evidence data into that database from your GitHub Codespace. Run the **load-cosmos.sh** script using the following command: + +```bash +"$walkthroughHome/challenge-02/cosmos/load-cosmos.sh" "$cosmosAccountName" +``` + +The script creates the containers as needed and imports the supplied data. A successful run includes output similar to this: + +```text +... +2026-08-26 13:16:48,487 INFO dim-bank complete: 2578 documents +... +2026-08-26 13:17:48,868 INFO fact-laundering-pattern-txn complete: 22743 documents +... +``` + +Open **Data Explorer** for the Cosmos DB account in the Azure portal. Explore documents in the `dim-bank` and `fact-laundering-pattern-txn` containers to understand the structure of the evidence available to the agent. + +### 2. Review and deploy the Financial Evidence MCP + +The **Financial Evidence MCP** provides read-only access to the evidence in Cosmos DB. It exposes tools that allow the Evidence Enrichment Agent to retrieve bank details, account history, individual transactions, and laundering-attempt timelines. + +```mermaid +flowchart LR + AGENT[Evidence Enrichment Agent] --> MCP[Azure Functions MCP endpoint] + + subgraph APP[function_app.py] + BANK[get_bank_information] + ACCOUNT[list_account_transactions] + TX[get_transaction] + ATTEMPT[get_laundering_attempt] + end + + MCP --> BANK & ACCOUNT & TX & ATTEMPT + BANK & ACCOUNT & TX & ATTEMPT --> REPO[CosmosRepository] + REPO --> BANKS[(dim-bank)] + REPO --> TRANSACTIONS[(fact-laundering-pattern-txn)] +``` + +Each MCP tool delegates data access to a shared repository. The repository reads from the appropriate Cosmos DB container and returns structured evidence to the agent. + +Key source files are: + +- `walkthrough/challenge-02/financial-evidence-mcp/src/function_app.py` - the Azure Functions app that implements the MCP tools. +- `walkthrough/challenge-02/financial-evidence-mcp/src/cosmos_repository.py` - the repository that interacts with Cosmos DB. + +Review these files to understand how the tools are implemented and how they interact with Cosmos DB. You can also run the MCP locally with Azure Functions Core Tools before deploying it. + +Deploy the MCP to Azure Functions from your Codespace: + +```bash +"$walkthroughHome/challenge-02/financial-evidence-mcp/deploy.sh" \ + -g "$rg" \ + -a "$cosmosAccountName" \ + -d "aml" \ + -l "$location" +``` + +Follow the progress in the terminal. The script creates the Azure Function App, configures its Cosmos DB settings, grants its managed identity the required read permissions, and publishes the MCP code. + +When deployment finishes, retrieve the MCP endpoint and the `mcp_extension` system key. You will use both values when connecting the MCP to the agent. + +Copy the endpoint printed by the deployment script: + +```text +MCP endpoint: https://.azurewebsites.net/runtime/webhooks/mcp +``` + +Add the endpoint to the `hackenv` file. The following commands retrieve the Function App name and build the full URL: + +```bash +functionAppName=$(az functionapp list --resource-group "$rg" --query "[?starts_with(name, 'mcp-financial-evidence')].name" --output tsv) +echo "financialEvidenceMcpEndpoint=https://$functionAppName.azurewebsites.net/runtime/webhooks/mcp" >> hackenv +``` + +In the Azure portal, open the Function App and select **Functions** > **App keys** > **System keys**. + +![MCP extension system key](/challenges/images/fin-evidence-mcp-key.png) + +Copy the value of the `mcp_extension` key and add it to `hackenv`: + +```bash +echo 'financialEvidenceMcpSystemKey=' >> hackenv +``` + +Load the new environment variables into your shell: + +```bash +source hackenv +``` + +### 3. Create the Evidence Enrichment Agent in Microsoft Foundry + +The agent will use the Financial Evidence MCP to retrieve evidence from Cosmos DB and enrich transactions with traceable facts. + +In the Azure portal, open your **Microsoft Foundry** resource and select **Go to Foundry Portal**. Sign in using your Hackbox credentials. + +![Microsoft Foundry login](/challenges/images/foundry-login.png) + +#### Explore the Foundry project + +The provided Microsoft Foundry project contains two model deployments: a chat model used by the agent and an embedding model. Find both deployments and review their details. + +Next, connect the Application Insights resource deployed in your environment to the Foundry project. This connection allows you to monitor agent performance and investigate issues. + +Open **Manage** and select **Project details** from the left menu. Select **Connected resources**, then **Add connection**. + +![Connected resources in Microsoft Foundry](/challenges/images/foundry-connected-resources.png) + +Select **Application Insights**, then select **Continue**. Choose the Application Insights resource deployed for your lab, leave **API key** as the authentication method, and select **Connect**. + +#### Create the agent + +Under **Build**, open **Agents**, select **New agent**, then select **Build an agent**. + +![Create a new agent in Microsoft Foundry](/challenges/images/foundry-new-agent.png) + +Name the agent `EvidenceEnrichmentAgent`. + +The new agent opens with a blank configuration: + +![Blank agent in Microsoft Foundry](/challenges/images/foundry-blank-agent.png) + +The chat model is selected automatically because it is the only deployed model that can power this agent. The embedding deployment is not a chat model. + +Select **Instructions** and copy the contents of `walkthrough/challenge-02/evidence-enrichment-agent/instructions.md` into the instruction editor. + +Review the instructions before continuing. They define how the agent handles input, invokes tools, reports missing evidence, and avoids making compliance decisions. + +Expand **Tools** and remove the existing **Web Search** tool because it is not an approved evidence source for this agent. + +Select **Add** > **Add tools**, open the **Custom** tab, select **Model Context Protocol**, then select **Create**. + +The MCP configuration form opens: + +![Configure MCP tool in Microsoft Foundry](/challenges/images/foundry-configure-mcp-tool.png) + +Configure these values: + +- **Name:** `financial-evidence-mcp` +- **Remote MCP endpoint:** Use the value returned by `echo "$financialEvidenceMcpEndpoint"`. +- **Authentication:** Leave **Key-based** selected. +- **Key:** `x-functions-key` +- **Value:** Use the value returned by `echo "$financialEvidenceMcpSystemKey"`. + +Select **Connect**. Foundry returns to the agent page and displays the new MCP tool. + +Open the MCP tool's `...` menu and select **Configure**: + +![Configure MCP tool menu in Microsoft Foundry](/challenges/images/foundry-configure-mcp-tool-menu.png) + +Then enable **Always auto-approve all tools**. + +![Configure auto approve for all tools in Microsoft Foundry](/challenges/images/foundry-auto-approve-tools.png) + +This setting allows the agent to use the MCP tools without requesting approval for every call. + +Select **Save**. Foundry creates a new version of the agent. + +### 4. Test and trace the agent + +Open the **Playground** to test the agent. + +![Test the agent in Microsoft Foundry Playground](/challenges/images/foundry-test-agent.png) + +Submit this transaction: + +```json +{ + "transaction_id": "TX-TEST-0001", + "originator_name": "James Carter", + "origin_account": "83D4B1F30", + "bank_origin": "0121", + "beneficiary_name": "Emily Foster", + "destination_account": "818CCA030", + "bank_destination": "29196", + "amount": 15000, + "currency": "EUR" +} +``` + +The response should be a JSON object that contains the original transaction enriched with relevant bank, account, historical transaction, and laundering evidence. + +For this example, the agent should find three pieces of evidence. To inspect the trace, scroll to the bottom and select **Traces**. It shows the agent calling the MCP tools according to its instructions and using the returned evidence to enrich the transaction. + +![Traces showing evidence data returned by the MCP tool](/challenges/images/foundry-traces-evidence.png) + +Select any **Execute tool** span to inspect the data returned by the MCP. + +## 🚀 Go Further + +Test a transaction with incomplete or conflicting evidence. Observe how the agent reports uncertainty and confirm that it does not fabricate evidence or make a compliance decision. + +Run the Financial Evidence MCP locally with Azure Functions Core Tools, invoke each tool directly, and compare the responses with the calls shown in the Foundry trace. + +## 🛠️ Troubleshooting + +- **The Cosmos import fails:** Confirm that `cosmosAccountName` contains the account name, that your Azure identity has Cosmos DB data-plane access, and that the account endpoint is reachable. +- **The MCP deployment fails:** Confirm that `rg`, `location`, and `cosmosAccountName` are loaded from `hackenv`, and verify that Azure Functions Core Tools is installed. +- **The MCP does not connect in Foundry:** Confirm that the endpoint ends with `/runtime/webhooks/mcp` and that the `x-functions-key` value is the `mcp_extension` system key. +- **No MCP tools appear:** Verify that the Function App deployment completed successfully, then reconnect the MCP tool in Foundry. +- **The agent does not call the MCP:** Confirm that the agent instructions were copied completely, Web Search was removed, the MCP tools are auto-approved, and the latest agent version was saved. +- **No traces appear:** Verify that the correct Application Insights resource is connected to the Foundry project, then run the test again. + +## 🧠 Conclusion + +You have built an Evidence Enrichment Agent that retrieves traceable financial evidence from Azure Cosmos DB through a read-only MCP. Continue to [Challenge 3](solution-03.md), where the enriched transaction will be assessed against global and regional regulations. diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/README_corpus_and_real_sources.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/README_corpus_and_real_sources.md new file mode 100644 index 000000000..4563aaa1d --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/README_corpus_and_real_sources.md @@ -0,0 +1,77 @@ +# Synthetic AML / CFT / KYC corpus for the Fraud Intelligence multi-agent hackathon + +15 PDFs + `index_manifest.json`. All content is **synthetic** — written for this hackathon, modelled on the structure and themes of public regulatory material, with no verbatim reproduction. Thresholds and country ratings are illustrative, not legal advice. + +## 1. Corpus layout + +### Global / supranational (always retrievable) +| File | ID | Jurisdiction | What the risk agent gets from it | +|---|---|---|---| +| 01_Global_AML_CFT_Standards_Summary | GLB-STD-001 | GLOBAL | Risk-based approach, CDD, wire-transfer transparency, core red flags | +| 03_Correspondent_Banking_Financial_Crime_Principles | GLB-CBK-003 | GLOBAL | Respondent/nested due diligence, correspondent monitoring, exit rules | +| 15_Typology_Casebook_and_Detection_Scenarios | GLB-TYP-015 | GLOBAL | Typologies as pseudo-rules with observable features + policy cross-references | + +### Bank-internal policy +| File | ID | Use | +|---|---|---| +| 04_KYC_CDD_Standard_Operating_Procedure | OPS-KYC-004 | Customer risk scoring model, UBO rules, SoF/SoW evidence table, alert workflow | +| 05_Jurisdiction_Risk_Matrix_and_Corridor_Rules | OPS-GEO-005 | **The routing document** — country ratings, corridor modifiers, controls per corridor, worked origin→destination examples | + +### Regional / local regulation — selected by transaction origin and destination +| File | ID | Jurisdiction | +|---|---|---| +| 02_EU_ML_TF_Risk_Factors_Guidelines | EU-GL-002 | EU | +| 06_Spain_AML_Local_Regulation | ES-REG-006 | ES | +| 07_Germany_AML_Local_Regulation | DE-REG-007 | DE | +| 08_UK_AML_Local_Regulation | GB-REG-008 | GB | +| 09_United_States_BSA_Local_Regulation | US-REG-009 | US | +| 10_Singapore_AML_Local_Regulation | SG-REG-010 | SG | +| 11_UAE_AML_Local_Regulation | AE-REG-011 | AE | +| 12_Mexico_AML_Local_Regulation | MX-REG-012 | MX | +| 13_Nigeria_AML_Local_Regulation | NG-REG-013 | NG | +| 14_Panama_AML_Local_Regulation | PA-REG-014 | PA | + +Each local document carries: scope, thresholds table, reporting duties, and a **local red flags** section — so the same transaction pattern produces different findings depending on the corridor. + +## 2. How the agents are meant to use it + +1. Fabric data agent returns candidate origin + destination accounts with their country codes. +2. Risk analyzer resolves the corridor rating from **OPS-GEO-005** (rating = max of both legs, plus modifiers). +3. It retrieves the local document for the **origin** jurisdiction and for the **destination** jurisdiction. +4. It matches observed behaviour against **GLB-TYP-015** scenarios. +5. It must cite `document_id` + section in the finding — the casebook makes an uncited assessment invalid, which is a clean guardrail to demo. + +## 3. Indexing in Azure AI Search / Foundry IQ + +Every PDF has a cover metadata table (Document ID, type, jurisdiction code, corridors, issuer, version) and matching PDF metadata (title/subject/keywords), so both text extraction and field mapping work. + +Suggested index fields: + +``` +id (key) doc_id (filterable) jurisdiction (filterable, facetable) +doctype (filterable) title (searchable) section (searchable) +content (searchable, vectorized) version +``` + +Filter pattern for the risk agent: + +``` +$filter=jurisdiction eq 'GLOBAL' or jurisdiction eq '{origin}' or jurisdiction eq '{destination}' +``` + +Chunking: split on the numbered headings (`1.`, `2.1`, …), 500–800 tokens with ~15% overlap; keep the section heading in each chunk so citations stay precise. `index_manifest.json` can drive an indexer's metadata or a custom skillset lookup. + +## 4. Real public sources worth adding alongside these + +These are genuine, freely downloadable and safe to index for a hackathon: + +- **FATF** — The FATF Recommendations (Feb 2025 consolidated), Methodology, and the typology/guidance library at fatf-gafi.org. +- **EBA** — Guidelines on ML/TF Risk Factors (EBA/GL/2021/02, consolidated with EBA/GL/2023/03) at eba.europa.eu. +- **ECB** — supervisory statements on AML/CFT interaction with prudential supervision (ecb.europa.eu, Banking Supervision section). +- **EUR-Lex** — Regulation (EU) 2015/847 / 2023/1113 on information accompanying transfers of funds and crypto-assets; AMLD5/6 and the 2024 AML package. +- **Wolfsberg Group** — Financial Crime Principles for Correspondent Banking, CBDDQ, Payment Transparency Standards. +- **Basel Committee (BIS)** — Sound management of risks related to money laundering and financing of terrorism. +- **Egmont Group / national FIUs** — typology and case reports (SEPBLAC in Spain, FinCEN advisories in the US, NCA SARs annual report in the UK). +- **UNODC / IMF** — money laundering and illicit financial flows studies. + +Mixing 3–5 real ones with these 15 synthetic ones gives you a realistic hybrid index without licensing worries on the generated content. diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/global/01_Global_AML_CFT_Standards_Summary.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/global/01_Global_AML_CFT_Standards_Summary.pdf new file mode 100644 index 000000000..e64a561cb Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/global/01_Global_AML_CFT_Standards_Summary.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/global/03_Correspondent_Banking_Financial_Crime_Principles.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/global/03_Correspondent_Banking_Financial_Crime_Principles.pdf new file mode 100644 index 000000000..f6a77f874 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/global/03_Correspondent_Banking_Financial_Crime_Principles.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/global/15_Typology_Casebook_and_Detection_Scenarios.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/global/15_Typology_Casebook_and_Detection_Scenarios.pdf new file mode 100644 index 000000000..598137aee Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/global/15_Typology_Casebook_and_Detection_Scenarios.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/global/20_Data_Scope_and_Rule_Applicability_Policy.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/global/20_Data_Scope_and_Rule_Applicability_Policy.pdf new file mode 100644 index 000000000..95c74ce93 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/global/20_Data_Scope_and_Rule_Applicability_Policy.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/global/21_Payment_Level_Detection_Rules_Catalogue.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/global/21_Payment_Level_Detection_Rules_Catalogue.pdf new file mode 100644 index 000000000..cb5101182 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/global/21_Payment_Level_Detection_Rules_Catalogue.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/index_manifest.json b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/index_manifest.json new file mode 100644 index 000000000..5ea1e9554 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/index_manifest.json @@ -0,0 +1,209 @@ +{ + "corpus": "synthetic-aml-kyc-hackathon", + "documents": [ + { + "id": "GLB-STD-001", + "file": "01_Global_AML_CFT_Standards_Summary.pdf", + "title": "Global AML/CFT Standards - Consolidated Summary of the Forty Recommendations", + "doctype": "International standard (summary)", + "jurisdiction": "GLOBAL", + "corridors": "All origin and destination jurisdictions", + "issuer": "International Financial Action Standards Board (simulated)", + "version": "v4.2 - effective 01 January 2026", + "basis": "FATF Recommendations and Methodology, publicly available", + "keywords": "FATF, AML, CFT, risk-based approach, CDD, beneficial ownership, wire transfer, STR" + }, + { + "id": "EU-GL-002", + "file": "02_EU_ML_TF_Risk_Factors_Guidelines.pdf", + "title": "Guidelines on Money Laundering and Terrorist Financing Risk Factors", + "doctype": "Supervisory guidelines", + "jurisdiction": "EU", + "corridors": "Intra-EU and EU inbound/outbound", + "issuer": "European Banking Supervision Authority (simulated)", + "version": "v3.0 - applicable from 30 June 2026", + "basis": "EBA Guidelines on ML/TF risk factors and the ECB supervisory approach, publicly available", + "keywords": "EU, EBA, ECB, risk factors, SDD, EDD, AMLD, AMLR, PSD2" + }, + { + "id": "GLB-CBK-003", + "file": "03_Correspondent_Banking_Financial_Crime_Principles.pdf", + "title": "Financial Crime Principles for Correspondent Banking and Nested Relationships", + "doctype": "Industry principles", + "jurisdiction": "GLOBAL", + "corridors": "Cross-border correspondent corridors", + "issuer": "Global Banking Financial Crime Forum (simulated)", + "version": "v2.4 - March 2026", + "basis": "Wolfsberg Group correspondent banking principles and BCBS sound management guidance, publicly available", + "keywords": "correspondent banking, respondent, nested, downstream clearing, MT103, payment transparency" + }, + { + "id": "OPS-KYC-004", + "file": "04_KYC_CDD_Standard_Operating_Procedure.pdf", + "title": "Know Your Customer and Customer Due Diligence - Standard Operating Procedure", + "doctype": "Internal operating procedure", + "jurisdiction": "GLOBAL", + "corridors": "All", + "issuer": "Northwind Commercial Bank - Financial Crime Compliance (fictional)", + "version": "v6.1 - effective 01 April 2026", + "basis": "FATF Recommendation 10, EU CDD requirements and common industry practice", + "keywords": "KYC, CDD, EDD, onboarding, beneficial ownership, UBO, risk score, periodic review" + }, + { + "id": "OPS-GEO-005", + "file": "05_Jurisdiction_Risk_Matrix_and_Corridor_Rules.pdf", + "title": "Jurisdiction Risk Matrix and Cross-Border Corridor Rules", + "doctype": "Risk methodology", + "jurisdiction": "GLOBAL", + "corridors": "Defines all corridor rules", + "issuer": "Northwind Commercial Bank - Financial Crime Compliance (fictional)", + "version": "v9.0 - reviewed quarterly, current cycle Q1 2026", + "basis": "Composite of public mutual evaluation outcomes, corruption and secrecy indices; ratings below are illustrative", + "keywords": "jurisdiction risk, corridor, origin, destination, country rating, high risk third country" + }, + { + "id": "ES-REG-006", + "file": "06_Spain_AML_Local_Regulation.pdf", + "title": "Spain - National Anti-Money Laundering Obligations for Obliged Entities", + "doctype": "National regulation summary", + "jurisdiction": "ES", + "corridors": "Transactions with origin or destination in ES", + "issuer": "Comision Nacional de Prevencion del Blanqueo (simulated)", + "version": "v1.0 - 2026 consolidated text (simulated)", + "basis": "Spanish Law 10/2010 and Royal Decree 304/2014 framework, and SEPBLAC guidance, publicly available", + "keywords": "Spain, SEPBLAC, Ley 10/2010, cash limit, DNI, NIE, titular real, comunicacion por indicio" + }, + { + "id": "DE-REG-007", + "file": "07_Germany_AML_Local_Regulation.pdf", + "title": "Germany - Money Laundering Act Obligations and Supervisory Expectations", + "doctype": "National regulation summary", + "jurisdiction": "DE", + "corridors": "Transactions with origin or destination in DE", + "issuer": "Bundesaufsicht fuer Finanzdienstleistungen (simulated)", + "version": "v1.0 - 2026 consolidated text (simulated)", + "basis": "German Geldwaeschegesetz (GwG) and BaFin interpretative guidance, publicly available", + "keywords": "Germany, GwG, BaFin, FIU, Transparenzregister, Bargeld, Verdachtsmeldung" + }, + { + "id": "GB-REG-008", + "file": "08_UK_AML_Local_Regulation.pdf", + "title": "United Kingdom - Money Laundering Regulations and Sector Guidance", + "doctype": "National regulation summary", + "jurisdiction": "GB", + "corridors": "Transactions with origin or destination in GB", + "issuer": "United Kingdom Financial Crime Supervisory Board (simulated)", + "version": "v1.0 - 2026 consolidated text (simulated)", + "basis": "UK Money Laundering Regulations 2017, POCA 2002 and JMLSG guidance, publicly available", + "keywords": "United Kingdom, MLR 2017, POCA, SAR, NCA, DAML, tipping off, JMLSG" + }, + { + "id": "US-REG-009", + "file": "09_United_States_BSA_Local_Regulation.pdf", + "title": "United States - Bank Secrecy Act Programme Requirements and Reporting Thresholds", + "doctype": "National regulation summary", + "jurisdiction": "US", + "corridors": "Transactions with origin or destination in US", + "issuer": "Federal Financial Crimes Supervision Bureau (simulated)", + "version": "v1.0 - 2026 consolidated text (simulated)", + "basis": "US Bank Secrecy Act, FinCEN CDD Rule and USA PATRIOT Act framework, publicly available", + "keywords": "United States, BSA, FinCEN, CTR, SAR, OFAC, 314(b), CDD rule, structuring" + }, + { + "id": "SG-REG-010", + "file": "10_Singapore_AML_Local_Regulation.pdf", + "title": "Singapore - AML/CFT Requirements for Banks and Payment Service Providers", + "doctype": "National regulation summary", + "jurisdiction": "SG", + "corridors": "Transactions with origin or destination in SG", + "issuer": "Monetary Supervision Authority of Singapore (simulated)", + "version": "v1.0 - 2026 consolidated text (simulated)", + "basis": "MAS Notice 626 and related MAS AML/CFT notices, publicly available", + "keywords": "Singapore, MAS, Notice 626, STRO, wire transfer, PEP, source of wealth" + }, + { + "id": "AE-REG-011", + "file": "11_UAE_AML_Local_Regulation.pdf", + "title": "United Arab Emirates - AML/CFT Obligations and Trade-Based Laundering Controls", + "doctype": "National regulation summary", + "jurisdiction": "AE", + "corridors": "Transactions with origin or destination in AE", + "issuer": "Federal Financial Integrity Authority (simulated)", + "version": "v1.0 - 2026 consolidated text (simulated)", + "basis": "UAE Federal Decree-Law on AML/CFT and central bank guidance, publicly available", + "keywords": "UAE, free zone, hawala, gold, trade-based money laundering, goAML, DNFBP" + }, + { + "id": "MX-REG-012", + "file": "12_Mexico_AML_Local_Regulation.pdf", + "title": "Mexico - Vulnerable Activities, Cash Restrictions and Reporting Obligations", + "doctype": "National regulation summary", + "jurisdiction": "MX", + "corridors": "Transactions with origin or destination in MX", + "issuer": "Comision Nacional de Integridad Financiera (simulated)", + "version": "v1.0 - 2026 consolidated text (simulated)", + "basis": "Mexican federal AML law on operations with illicit proceeds (LFPIORPI) and CNBV rules, publicly available", + "keywords": "Mexico, LFPIORPI, CNBV, UIF, actividades vulnerables, dolares en efectivo, aviso" + }, + { + "id": "NG-REG-013", + "file": "13_Nigeria_AML_Local_Regulation.pdf", + "title": "Nigeria - AML/CFT Regulations, Cash Thresholds and Fraud Proceeds Typologies", + "doctype": "National regulation summary", + "jurisdiction": "NG", + "corridors": "Transactions with origin or destination in NG", + "issuer": "National Financial Integrity Commission (simulated)", + "version": "v1.0 - 2026 consolidated text (simulated)", + "basis": "Nigerian Money Laundering (Prevention and Prohibition) Act and CBN AML/CFT regulations, publicly available", + "keywords": "Nigeria, CBN, NFIU, BVN, currency transaction report, mule account, fraud proceeds" + }, + { + "id": "PA-REG-014", + "file": "14_Panama_AML_Local_Regulation.pdf", + "title": "Panama - Obligations for Financial and Non-Financial Entities and Corporate Transparency", + "doctype": "National regulation summary", + "jurisdiction": "PA", + "corridors": "Transactions with origin or destination in PA", + "issuer": "Superintendencia de Integridad Financiera (simulated)", + "version": "v1.0 - 2026 consolidated text (simulated)", + "basis": "Panamanian AML law framework and beneficial ownership registry legislation, publicly available", + "keywords": "Panama, resident agent, offshore, beneficial ownership registry, free zone, bearer shares" + }, + { + "id": "IL-REG-016", + "file": "16_Israel_AML_Local_Regulation.pdf", + "title": "Israel - Prohibition on Money Laundering Obligations and Cash Use Restrictions", + "doctype": "National regulation summary", + "jurisdiction": "IL", + "corridors": "Transactions with origin or destination in IL", + "issuer": "National Authority for the Prohibition of Money Laundering (simulated)", + "version": "v1.0 - 2026 consolidated text (simulated)", + "basis": "Israeli Prohibition on Money Laundering Law framework, the Law for the Reduction of the Use of Cash, and Bank of Israel supervisory orders, publicly available", + "keywords": "Israel, IMPA, prohibition on money laundering, cash reduction law, diamonds, NPO, shekel, reporting order" + }, + { + "id": "BD-REG-017", + "file": "17_Bangladesh_AML_Local_Regulation.pdf", + "title": "Bangladesh - Money Laundering Prevention Obligations, Remittance and Trade Controls", + "doctype": "National regulation summary", + "jurisdiction": "BD", + "corridors": "Transactions with origin or destination in BD", + "issuer": "National Financial Intelligence Unit of Bangladesh (simulated)", + "version": "v1.0 - 2026 consolidated text (simulated)", + "basis": "Bangladesh Money Laundering Prevention Act framework, Anti-Terrorism Act, BFIU circulars and foreign exchange regulation, publicly available", + "keywords": "Bangladesh, BFIU, MLPA, hundi, remittance, ready-made garments, trade-based, mobile financial services, CTR, STR" + }, + { + "id": "GLB-TYP-015", + "file": "15_Typology_Casebook_and_Detection_Scenarios.pdf", + "title": "Typology Casebook and Detection Scenario Library", + "doctype": "Typology reference", + "jurisdiction": "GLOBAL", + "corridors": "All", + "issuer": "Financial Crime Analytics Centre (simulated)", + "version": "v1.3 - February 2026", + "basis": "Public FATF and Egmont Group typology literature; all cases below are fictional", + "keywords": "typology, scenario, structuring, smurfing, mule, trade-based, layering, integration, detection rule" + } + ] +} \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/internal/04_KYC_CDD_Standard_Operating_Procedure.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/internal/04_KYC_CDD_Standard_Operating_Procedure.pdf new file mode 100644 index 000000000..b6aa53c89 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/internal/04_KYC_CDD_Standard_Operating_Procedure.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/internal/05_Jurisdiction_Risk_Matrix_and_Corridor_Rules.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/internal/05_Jurisdiction_Risk_Matrix_and_Corridor_Rules.pdf new file mode 100644 index 000000000..1f98adf0a Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/internal/05_Jurisdiction_Risk_Matrix_and_Corridor_Rules.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/02_EU_ML_TF_Risk_Factors_Guidelines.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/02_EU_ML_TF_Risk_Factors_Guidelines.pdf new file mode 100644 index 000000000..a8c0e59a0 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/02_EU_ML_TF_Risk_Factors_Guidelines.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/06_Spain_AML_Local_Regulation.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/06_Spain_AML_Local_Regulation.pdf new file mode 100644 index 000000000..3610a37e1 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/06_Spain_AML_Local_Regulation.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/07_Germany_AML_Local_Regulation.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/07_Germany_AML_Local_Regulation.pdf new file mode 100644 index 000000000..6bbcf5129 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/07_Germany_AML_Local_Regulation.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/08_UK_AML_Local_Regulation.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/08_UK_AML_Local_Regulation.pdf new file mode 100644 index 000000000..e478d64a3 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/08_UK_AML_Local_Regulation.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/09_United_States_BSA_Local_Regulation.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/09_United_States_BSA_Local_Regulation.pdf new file mode 100644 index 000000000..b3bd98aff Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/09_United_States_BSA_Local_Regulation.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/10_Singapore_AML_Local_Regulation.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/10_Singapore_AML_Local_Regulation.pdf new file mode 100644 index 000000000..e9701846a Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/10_Singapore_AML_Local_Regulation.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/11_UAE_AML_Local_Regulation.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/11_UAE_AML_Local_Regulation.pdf new file mode 100644 index 000000000..4fb322dd4 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/11_UAE_AML_Local_Regulation.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/12_Mexico_AML_Local_Regulation.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/12_Mexico_AML_Local_Regulation.pdf new file mode 100644 index 000000000..19a0832f5 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/12_Mexico_AML_Local_Regulation.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/13_Nigeria_AML_Local_Regulation.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/13_Nigeria_AML_Local_Regulation.pdf new file mode 100644 index 000000000..815c26d8c Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/13_Nigeria_AML_Local_Regulation.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/14_Panama_AML_Local_Regulation.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/14_Panama_AML_Local_Regulation.pdf new file mode 100644 index 000000000..e3a8870b6 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/14_Panama_AML_Local_Regulation.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/16_Israel_AML_Local_Regulation.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/16_Israel_AML_Local_Regulation.pdf new file mode 100644 index 000000000..d93532c53 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/16_Israel_AML_Local_Regulation.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/17_Bangladesh_AML_Local_Regulation.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/17_Bangladesh_AML_Local_Regulation.pdf new file mode 100644 index 000000000..803472131 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/17_Bangladesh_AML_Local_Regulation.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/18_Belgium_AML_Local_Regulation.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/18_Belgium_AML_Local_Regulation.pdf new file mode 100644 index 000000000..0292ebc46 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/18_Belgium_AML_Local_Regulation.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/19_China_AML_Local_Regulation.pdf b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/19_China_AML_Local_Regulation.pdf new file mode 100644 index 000000000..8c7a61ef2 Binary files /dev/null and b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/documents/regional/19_China_AML_Local_Regulation.pdf differ diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/regulatory-assesment-agent/instructions.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/regulatory-assesment-agent/instructions.md new file mode 100644 index 000000000..383e1c958 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/regulatory-assesment-agent/instructions.md @@ -0,0 +1,831 @@ +## Purpose + +You are `RegulatoryAssessmentAgent`. + +Your responsibility is to validate whether an enriched transaction context complies with AML regulations, supranational policies, global AML obligations, and local policies applicable to the origin and destination countries. + +You must use the knowledge base named `kb-aml` as the only source for AML regulations, policies, regulatory obligations, compliance rules, local requirements, and supranational guidance. + +You must append a new `aml_regulatory_assessment` element to the input JSON. + +You are not a fraud detection agent. +You are not a case recommendation agent. +You are not an alerting agent. +You are a regulation and policy validation agent. + +--- + +## Input + +The input already contains: + +- Original transaction information. +- Origin account. +- Destination account. +- Originator name. +- Beneficiary name. +- Origin bank. +- Destination bank. +- Origin country. +- Destination country. +- Historical AML participation. +- Evidence summaries. +- Historical context level. +- Pattern diversity. +- Recency indicators. +- Account participation roles. +- Derived AML historical features. + +The agent must not remove, rewrite, or lose any existing input fields. +Treat party names as supplied identity data, not as evidence that identity verification, customer due diligence, or wire-transfer information requirements are complete. + +### Example Input + +```json +{ + "transaction_id": "TX-TEST-0001", + "origin_account": { + "account": "83D4B1F30", + "originator_name": "James Carter", + "bank_id": "0121", + "bank_name": "Israel Bank #35", + "country": "Israel", + "historical_participation": true, + "role": "Receiver", + "laundering_transaction_count": 3, + "laundering_attempt_count": 3, + "first_seen": "2026-09-02T10:03:00Z", + "last_seen": "2026-09-18T05:02:00Z", + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "historical_context_level": 4, + "historical_context_category": "Repeated Participation Across Multiple AML Patterns" + }, + "destination_account": { + "account": "818CCA030", + "beneficiary_name": "Emily Foster", + "bank_id": "29196", + "bank_name": "Bank of Topeka", + "country": "Bangladesh", + "historical_participation": false, + "role": null, + "laundering_transaction_count": 0, + "laundering_attempt_count": 0, + "first_seen": null, + "last_seen": null, + "pattern_types": [], + "historical_context_level": 0, + "historical_context_category": "No Historical AML Participation" + }, + "historical_context": { + "accounts_with_history": 1, + "accounts_without_history": 1, + "highest_historical_context_level": 4, + "transaction_interpretation": "One account has repeated historical AML participation across multiple laundering patterns." + }, + "derived_features": { + "origin_has_history": true, + "destination_has_history": false, + "multiple_pattern_presence": true, + "recent_historical_activity": true, + "highest_historical_context_level": 4 + } +} + +``` + +--- + +## Knowledge Base Usage + +You must query kb-aml for all applicable AML regulatory and policy rules. + +You must retrieve rules from the following scenarios: + +### 1. Global AML Scenario + +Retrieve global and supranational AML policies that apply regardless of country. + +Use this for: + +- General AML obligations. +- Customer due diligence. +- Enhanced due diligence. +- Transaction monitoring. +- Suspicious activity reporting requirements. +- Record keeping. +- Sanctions screening. +- Cross-border transaction controls. +- High-risk typology guidance. +- FATF-style guidance. +- Supranational AML frameworks. + +### 2. Origin Country Scenario + +Retrieve AML policies and rules that apply to the origin country. + +Use the country from: + +`origin_account.country` + +### 3. Destination Country Scenario + +Retrieve AML policies and rules that apply to the destination country. + +Use the country from: + +`destination_account.country` + +### 4. Cross-Border Scenario + +If origin country and destination country are different, retrieve rules for cross-border AML obligations. + +### 5. Historical AML Context Scenario + +If the transaction includes accounts with historical AML participation, retrieve policies that apply to: + +- Previously identified laundering accounts. +- Repeated AML involvement. +- Historical laundering patterns. +- Known laundering typologies. +- High historical context levels. +- Accounts appearing in confirmed AML activity. +- Repeated participation across multiple AML patterns. + +### 6. Bank and Jurisdiction Scenario + +Retrieve any rules related to banks, jurisdictions, or country-specific controls when bank country or country information is present. + +## Required Knowledge Base Queries + +For every input, query kb-aml using multiple focused searches. + +The agent must issue searches equivalent to: + +```text +global AML regulations transaction monitoring customer due diligence suspicious activity reporting + +supranational AML policies FATF transaction monitoring historical laundering patterns + +AML policy for origin country {origin_country} + +AML policy for destination country {destination_country} + +cross-border AML obligations origin country {origin_country} destination country {destination_country} + +AML rules for accounts with previous laundering history repeated participation laundering typologies + +AML policy for historical participation level {highest_historical_context_level} +``` + +When origin country and destination country are the same, do not create a cross-border finding unless the retrieved policy explicitly applies. + +--- + +## Regulatory Validation Logic + +For each retrieved rule or policy: + +- Identify the rule. +- Identify the regulatory scope. +- Determine whether the rule applies to the transaction. +- Validate the transaction against the rule using only the input JSON and retrieved knowledge base content. +- Classify the compliance result. +- Explain the reasoning. +- Include the evidence fields used from the input JSON. +- Include the knowledge base source or policy reference if available. + +## Compliance Status Values + +Use only the following values: + +- `COMPLIANT` +- `NON_COMPLIANT` +- `POTENTIAL_GAP` +- `NOT_APPLICABLE` +- `INSUFFICIENT_DATA` + +### `COMPLIANT` + +Use when the input provides enough evidence to determine that the transaction satisfies the rule. + +### `NON_COMPLIANT` + +Use when the input clearly violates a retrieved AML rule or policy. + +### `POTENTIAL_GAP` + +Use when the input indicates a possible policy gap, missing control, missing validation, or potential policy mismatch, but the available evidence is not strong enough to declare non-compliance. + +### `NOT_APPLICABLE` + +Use when the retrieved rule does not apply to this transaction scenario. + +### `INSUFFICIENT_DATA` + +Use when the rule may apply, but the required data is missing from the input JSON. + +--- + +## Important Constraints + +Do not invent regulations. + +Do not create AML requirements from general knowledge. + +Do not assume local rules. + +Do not use regulatory knowledge that was not retrieved from kb-aml. + +If kb-aml does not return a relevant rule, say that no applicable rule was retrieved. + +## What the Agent Must Validate + +The agent should validate rules against the enriched evidence, including: + +- Origin country. +- Destination country. +- Whether origin and destination countries differ. +- Origin bank. +- Destination bank. +- Transaction amount. +- Transaction currency. +- Historical AML participation. +- Historical context level. +- AML pattern types. +- Repeated participation. +- Recent historical activity. +- Account role in historical AML evidence. +- Number of laundering attempts. +- Whether one or both accounts have historical AML evidence. + +## Interpretation Rules + +The agent may say: + +> The transaction shows regulatory relevance because one account has historical AML participation and the knowledge base contains policies requiring additional review for accounts previously linked to laundering activity. + + +The agent must not say: + +> The transaction is fraudulent. + + +The agent must not say: + +> The customer is laundering money. + + +The agent must not say: + +> The transaction must be blocked. + + +The agent may identify non-compliance only when a retrieved policy explicitly supports that conclusion. + +--- + +## Reusable Decision-Support Calculations + +The Regulatory Assessment Agent owns all deterministic scoring and workflow classification calculations. Downstream agents must consume these values and must not recalculate them. + +### Calculation Inputs + +Use: + +- `highest_historical_context_level` from `derived_features.highest_historical_context_level`, falling back to `historical_context.highest_historical_context_level` and then `aml_regulatory_inputs.historical_context_level`. +- Rule counts from `aml_regulatory_assessment.overall_compliance`. + +### Investigation Priority Score + +Use this base score: + +| Historical context level | Base score | +| ---: | ---: | +| 0 | 0 | +| 1 | 10 | +| 2 | 20 | +| 3 | 35 | +| 4 | 50 | +| 5 | 65 | + +Add: + +- 10 points for each `POTENTIAL_GAP` rule. +- 5 points for each `INSUFFICIENT_DATA` rule. +- 25 points for each `NON_COMPLIANT` rule. + +Set `raw_score` to the sum of the base score and all additions. Set `score` to `MIN(raw_score, 100)`. + +Map `score` to `priority_band`: + +| Score | Priority band | +| ---: | --- | +| 0-19 | `Low` | +| 20-39 | `Moderate` | +| 40-59 | `Elevated` | +| 60-79 | `High` | +| 80-100 | `Critical` | + +This is an investigation workflow priority score. It is not an AML risk score or a probability of money laundering. + +### Deterministic Final Verdict + +Evaluate these rules in order. The first matching rule wins. + +| Rule ID | Final verdict | Condition | +| --- | --- | --- | +| `LEVEL_E` | `CRITICAL REGULATORY ESCALATION` | `non_compliant_rule_count >= 3` OR (`non_compliant_rule_count >= 1` AND `highest_historical_context_level >= 4`) | +| `LEVEL_D` | `REGULATORY ESCALATION RECOMMENDED` | `non_compliant_rule_count >= 1` | +| `LEVEL_C` | `ENHANCED REVIEW REQUIRED` | `highest_historical_context_level >= 4` AND (`potential_gap_rule_count >= 1` OR `insufficient_data_rule_count >= 2`) AND `non_compliant_rule_count = 0` | +| `LEVEL_B` | `REGULATORY REVIEW REQUIRED` | `non_compliant_rule_count = 0` AND `potential_gap_rule_count >= 1` AND `highest_historical_context_level < 4` | +| `LEVEL_A` | `REGULATORY COMPLIANT` | `non_compliant_rule_count = 0` AND `potential_gap_rule_count = 0` AND `insufficient_data_rule_count <= 1` AND `highest_historical_context_level <= 2` | +| `FALLBACK` | `REGULATORY REVIEW REQUIRED` | No preceding rule matches | + +The verdict is a workflow classification, not a legal conclusion. It does not establish fraud, money laundering, criminal activity, or a mandatory operational action. + +--- + +## Output Requirements + +The output must be valid JSON. + +The output must preserve the complete input JSON. + +The output must append a new top-level element: + +```json +{ + "aml_regulatory_assessment": {} +} +``` + + +The output must include: + +- Retrieved regulatory scenarios. +- Applied rules. +- Per-rule compliance validation. +- Overall compliance result. +- Regulatory interpretation. +- Missing data. +- Sources retrieved from `kb-aml`. +- Fields used from the evidence enrichment input. + +### Output JSON Schema + +```json +{ + "transaction_id": "string", + + "origin_account": {}, + "destination_account": {}, + "historical_context": {}, + "derived_features": {}, + + "aml_regulatory_assessment": { + "assessment_source": "kb-aml", + "assessment_type": "AML regulation and policy compliance validation", + + "regulatory_scope": { + "global_rules_evaluated": true, + "supranational_rules_evaluated": true, + "origin_country_rules_evaluated": true, + "destination_country_rules_evaluated": true, + "cross_border_rules_evaluated": true, + "historical_aml_context_rules_evaluated": true, + "origin_country": "string", + "destination_country": "string", + "is_cross_border": true + }, + + "retrieved_policy_sets": [ + { + "policy_set_id": "string", + "policy_set_name": "string", + "scope": "GLOBAL | SUPRANATIONAL | ORIGIN_COUNTRY | DESTINATION_COUNTRY | CROSS_BORDER | HISTORICAL_AML_CONTEXT", + "jurisdiction": "string or null", + "summary": "string", + "source_reference": "string or null" + } + ], + + "rule_evaluations": [ + { + "rule_id": "string", + "rule_name": "string", + "policy_set_name": "string", + "scope": "GLOBAL | SUPRANATIONAL | ORIGIN_COUNTRY | DESTINATION_COUNTRY | CROSS_BORDER | HISTORICAL_AML_CONTEXT", + "jurisdiction": "string or null", + "rule_summary": "string", + + "applies_to_transaction": true, + "compliance_status": "COMPLIANT | NON_COMPLIANT | POTENTIAL_GAP | NOT_APPLICABLE | INSUFFICIENT_DATA", + + "validation_result": { + "passed": true, + "failed": false, + "missing_required_data": false + }, + + "evidence_used": { + "transaction_id": "string", + "origin_country": "string", + "destination_country": "string", + "origin_bank_id": "string", + "destination_bank_id": "string", + "amount": "number or null", + "currency": "string or null", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [], + "recent_historical_activity": true + }, + + "interpretation": "Factual explanation of why the rule is compliant, non-compliant, potentially incomplete, not applicable, or cannot be assessed.", + "kb_source_reference": "string or null" + } + ], + + "overall_compliance": { + "overall_status": "COMPLIANT | NON_COMPLIANT | POTENTIAL_GAP | INSUFFICIENT_DATA", + "non_compliant_rule_count": 0, + "potential_gap_rule_count": 0, + "insufficient_data_rule_count": 0, + "not_applicable_rule_count": 0, + "compliant_rule_count": 0 + }, + + "decision_support": { + "calculation_version": "1.0", + "calculation_inputs": { + "highest_historical_context_level": 0, + "non_compliant_rule_count": 0, + "potential_gap_rule_count": 0, + "insufficient_data_rule_count": 0, + "not_applicable_rule_count": 0, + "compliant_rule_count": 0 + }, + "investigation_priority": { + "base_historical_context_score": 0, + "potential_gap_points": 0, + "insufficient_data_points": 0, + "non_compliance_points": 0, + "raw_score": 0, + "score": 0, + "maximum_score": 100, + "priority_band": "Low" + }, + "workflow_classification": { + "final_verdict": "REGULATORY COMPLIANT | REGULATORY REVIEW REQUIRED | ENHANCED REVIEW REQUIRED | REGULATORY ESCALATION RECOMMENDED | CRITICAL REGULATORY ESCALATION", + "matched_rule_id": "LEVEL_A | LEVEL_B | LEVEL_C | LEVEL_D | LEVEL_E | FALLBACK", + "rationale": [ + "string" + ], + "meaning": "string", + "is_legal_conclusion": false, + "establishes_money_laundering": false + } + }, + + "regulatory_interpretation": { + "summary": "string", + "key_findings": [ + "string" + ], + "regulatory_relevance": "string" + }, + + "missing_data": [ + { + "field": "string", + "required_for_rule": "string", + "reason": "string" + } + ], + + "downstream_inputs": { + "has_regulatory_non_compliance": false, + "has_potential_regulatory_gap": true, + "requires_case_recommendation_review": true, + "highest_historical_context_level": 4, + "rules_triggered": [], + "countries_evaluated": [], + "policy_scopes_evaluated": [] + } + } +} + +``` + +### Example Output + +```json +{ + "transaction_id": "TX-TEST-0001", + "origin_account": { + "account": "83D4B1F30", + "originator_name": "James Carter", + "bank_id": "0121", + "bank_name": "Israel Bank #35", + "country": "Israel", + "historical_participation": true, + "role": "Receiver", + "laundering_transaction_count": 3, + "laundering_attempt_count": 3, + "first_seen": "2026-09-02T10:03:00Z", + "last_seen": "2026-09-18T05:02:00Z", + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "historical_context_level": 4, + "historical_context_category": "Repeated Participation Across Multiple AML Patterns" + }, + "destination_account": { + "account": "818CCA030", + "beneficiary_name": "Emily Foster", + "bank_id": "29196", + "bank_name": "Bank of Topeka", + "country": "Bangladesh", + "historical_participation": false, + "role": null, + "laundering_transaction_count": 0, + "laundering_attempt_count": 0, + "first_seen": null, + "last_seen": null, + "pattern_types": [], + "historical_context_level": 0, + "historical_context_category": "No Historical AML Participation" + }, + "historical_context": { + "accounts_with_history": 1, + "accounts_without_history": 1, + "highest_historical_context_level": 4, + "transaction_interpretation": "One account has repeated historical AML participation across multiple laundering patterns." + }, + "derived_features": { + "origin_has_history": true, + "destination_has_history": false, + "multiple_pattern_presence": true, + "recent_historical_activity": true, + "highest_historical_context_level": 4 + }, + "aml_regulatory_assessment": { + "assessment_source": "kb-aml", + "assessment_type": "AML regulation and policy compliance validation", + "regulatory_scope": { + "global_rules_evaluated": true, + "supranational_rules_evaluated": true, + "origin_country_rules_evaluated": true, + "destination_country_rules_evaluated": true, + "cross_border_rules_evaluated": true, + "historical_aml_context_rules_evaluated": true, + "origin_country": "Israel", + "destination_country": "Bangladesh", + "is_cross_border": true + }, + "retrieved_policy_sets": [ + { + "policy_set_id": "KB-AML-GLOBAL-001", + "policy_set_name": "Global AML Transaction Monitoring Policy", + "scope": "GLOBAL", + "jurisdiction": null, + "summary": "Global policy covering monitoring of transactions involving historical AML indicators.", + "source_reference": "kb-aml/global-transaction-monitoring" + }, + { + "policy_set_id": "KB-AML-CROSS-BORDER-001", + "policy_set_name": "Cross-Border AML Review Policy", + "scope": "CROSS_BORDER", + "jurisdiction": "Israel to Bangladesh", + "summary": "Policy covering AML checks for cross-border transactions involving different jurisdictions.", + "source_reference": "kb-aml/cross-border-review" + }, + { + "policy_set_id": "KB-AML-HISTORICAL-001", + "policy_set_name": "Previously Linked Account AML Policy", + "scope": "HISTORICAL_AML_CONTEXT", + "jurisdiction": null, + "summary": "Policy covering enhanced review requirements when an account has previous confirmed AML involvement.", + "source_reference": "kb-aml/historical-aml-participation" + } + ], + "rule_evaluations": [ + { + "rule_id": "AML-GLOBAL-TRM-001", + "rule_name": "Transaction monitoring for accounts with historical AML evidence", + "policy_set_name": "Global AML Transaction Monitoring Policy", + "scope": "GLOBAL", + "jurisdiction": null, + "rule_summary": "Transactions involving accounts with prior AML evidence must be evaluated against historical participation indicators.", + "applies_to_transaction": true, + "compliance_status": "COMPLIANT", + "validation_result": { + "passed": true, + "failed": false, + "missing_required_data": false + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": null, + "currency": null, + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "recent_historical_activity": true + }, + "interpretation": "The rule applies because the origin account has confirmed historical AML participation. The enriched evidence contains the required historical indicators, including attempt count, pattern types, and recent activity.", + "kb_source_reference": "kb-aml/global-transaction-monitoring" + }, + { + "rule_id": "AML-CROSS-BORDER-002", + "rule_name": "Cross-border jurisdiction policy check", + "policy_set_name": "Cross-Border AML Review Policy", + "scope": "CROSS_BORDER", + "jurisdiction": "Israel to Bangladesh", + "rule_summary": "Cross-border transactions must be checked against origin and destination country AML policy requirements.", + "applies_to_transaction": true, + "compliance_status": "POTENTIAL_GAP", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": null, + "currency": null, + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "recent_historical_activity": true + }, + "interpretation": "The transaction is cross-border because origin and destination countries differ. The input does not include all fields required to validate the full cross-border policy, so this is marked as a potential regulatory gap rather than non-compliance.", + "kb_source_reference": "kb-aml/cross-border-review" + }, + { + "rule_id": "AML-HIST-004", + "rule_name": "Repeated historical AML participation review", + "policy_set_name": "Previously Linked Account AML Policy", + "scope": "HISTORICAL_AML_CONTEXT", + "jurisdiction": null, + "rule_summary": "Accounts with repeated participation across multiple AML patterns require historical AML context validation.", + "applies_to_transaction": true, + "compliance_status": "COMPLIANT", + "validation_result": { + "passed": true, + "failed": false, + "missing_required_data": false + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": null, + "currency": null, + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "recent_historical_activity": true + }, + "interpretation": "The rule applies because the origin account has repeated historical AML participation across three distinct laundering pattern types. The required historical context is present in the enriched input.", + "kb_source_reference": "kb-aml/historical-aml-participation" + } + ], + "overall_compliance": { + "overall_status": "POTENTIAL_GAP", + "non_compliant_rule_count": 0, + "potential_gap_rule_count": 1, + "insufficient_data_rule_count": 0, + "not_applicable_rule_count": 0, + "compliant_rule_count": 2 + }, + "decision_support": { + "calculation_version": "1.0", + "calculation_inputs": { + "highest_historical_context_level": 4, + "non_compliant_rule_count": 0, + "potential_gap_rule_count": 1, + "insufficient_data_rule_count": 0, + "not_applicable_rule_count": 0, + "compliant_rule_count": 2 + }, + "investigation_priority": { + "base_historical_context_score": 50, + "potential_gap_points": 10, + "insufficient_data_points": 0, + "non_compliance_points": 0, + "raw_score": 60, + "score": 60, + "maximum_score": 100, + "priority_band": "High" + }, + "workflow_classification": { + "final_verdict": "ENHANCED REVIEW REQUIRED", + "matched_rule_id": "LEVEL_C", + "rationale": [ + "The highest historical context level is 4.", + "One potential regulatory gap remains unresolved.", + "No rule was assessed as non-compliant." + ], + "meaning": "The transaction has significant historical AML relevance and unresolved regulatory validation gaps, but no confirmed regulatory breach was identified.", + "is_legal_conclusion": false, + "establishes_money_laundering": false + } + }, + "regulatory_interpretation": { + "summary": "The transaction has regulatory relevance because one account has repeated historical AML participation and the transaction involves two different countries. No direct non-compliance is established from the available evidence, but one cross-border validation gap remains because additional data required by the retrieved policy is not present.", + "key_findings": [ + "Origin account has historical AML participation.", + "Origin account appears across multiple AML pattern types.", + "Destination account has no historical AML participation in the enriched evidence.", + "Origin and destination countries are different.", + "Cross-border AML policy validation requires additional fields not present in the input." + ], + "regulatory_relevance": "This assessment identifies applicable AML policy checks and validates the transaction against retrieved rules from kb-aml. It does not determine fraud, suspicious activity, or case outcome." + }, + "missing_data": [ + { + "field": "customer_due_diligence_status", + "required_for_rule": "AML-CROSS-BORDER-002", + "reason": "The retrieved cross-border policy requires CDD status, but the field is not present in the enriched input." + }, + { + "field": "sanctions_screening_result", + "required_for_rule": "AML-CROSS-BORDER-002", + "reason": "The retrieved cross-border policy requires sanctions screening result, but the field is not present in the enriched input." + } + ], + "downstream_inputs": { + "has_regulatory_non_compliance": false, + "has_potential_regulatory_gap": true, + "requires_case_recommendation_review": true, + "highest_historical_context_level": 4, + "rules_triggered": [ + "AML-GLOBAL-TRM-001", + "AML-CROSS-BORDER-002", + "AML-HIST-004" + ], + "countries_evaluated": [ + "Israel", + "Bangladesh" + ], + "policy_scopes_evaluated": [ + "GLOBAL", + "SUPRANATIONAL", + "ORIGIN_COUNTRY", + "DESTINATION_COUNTRY", + "CROSS_BORDER", + "HISTORICAL_AML_CONTEXT" + ] + } + } +} + +``` + +--- + +## Final Instructions + +- Always preserve the full input JSON. +- Always append the `aml_regulatory_assessment` object. +- Always use `kb-aml` as the only source for regulations and policies. +- Always retrieve global, supranational, origin country, destination country, cross-border, and historical AML context rules when applicable. +- Always calculate and return the complete `decision_support` object. +- Keep scoring and verdict calculations deterministic and consistent with the rules in this document. +- Never invent regulations. +- Never issue fraud verdicts. +- Never recommend operational actions. +- Only validate regulatory and policy compliance. diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/solution-03.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/solution-03.md new file mode 100644 index 000000000..99b8a09c5 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-03/solution-03.md @@ -0,0 +1,426 @@ +# Challenge 3 - Build the Regulatory Assessment Agent + +[Previous challenge](solution-02.md) | **[Home](../README.md)** | [Next challenge](solution-04.md) + +## 🎯 Objective + +Configure a Foundry IQ knowledge base containing global, internal, and regional AML policies, then build a **Regulatory Assessment Agent** that applies the rules relevant to both sides of an enriched transaction. + +## 🧭 Context and Background + +The Evidence Enrichment Agent from Challenge 2 retrieves facts but does not decide whether a transaction complies with policy. The Regulatory Assessment Agent receives that evidence, determines the origin and destination countries, retrieves the applicable guidance, and returns a cited assessment. + +```mermaid +flowchart LR + TX[Enriched transaction] --> AGENT[Regulatory Assessment Agent] + AGENT --> IQ[Foundry IQ agentic retrieval] + IQ --> GLOBAL[Global AML guidance] + IQ --> INTERNAL[Internal policy] + IQ --> ORIGIN[Origin-country rules] + IQ --> DEST[Destination-country rules] +``` + +The assessment must remain grounded in retrieved policy. Missing or conflicting guidance must be reported explicitly rather than resolved by assumption. + +## ✅ Tasks + +Before starting, source `baseenv` and `hackenv` to load the environment variables used by this challenge. + +```bash +source baseenv +source hackenv +``` + +### 1. Create the Foundry IQ knowledge base + +Build the Foundry IQ knowledge base that will support the Regulatory Assessment Agent. This involves ingesting relevant policy documents, providing retrieval instructions, and ensuring that the documents are searchable during assessments. + +In **Foundry**, open the **Build** section. Then select **Knowledge** in the left navigation pane to open the main interface for managing your knowledge base: + +![Foundry IQ Knowledge](/challenges/images/foundry-iq-knowledge.png) + +To learn more about how Foundry IQ works, watch the video available on this page. + +At the bottom of the page, select **Create new resource** to start building your knowledge base. + +Use the defaults, accept the acknowledgment, and proceed to create the new knowledge base resource: + +![Foundry IQ Create New Resource](/challenges/images/foundry-iq-create-new-resource.png) + +Under the hood, a new **Azure AI Search** resource is being created to support the knowledge base. This resource will handle the indexing and retrieval of policy documents, ensuring that the Regulatory Assessment Agent can access the necessary information efficiently. + +After the resource is created, select **Create a knowledge base** and set the following values: + +- **Name**: kb-aml +- **Description**: Synthetic AML / CFT / KYC corpus for Fraud Intelligence +- **Chat completion model**: gpt-5-mini +- **Retrieval reasoning effort**: Minimal +- **Output mode**: Extractive data +- **Retrieval instructions**: You have to always retrieve first the global policies and rules. + +![Foundry IQ Knowledge Base Creation](/challenges/images/foundry-iq-knowledge-base-creation.png) + +Then select **Add sources** and review the available source types for your knowledge base. + +![Foundry IQ Add Sources](/challenges/images/foundry-iq-add-sources.png) + +Stop here for now. The next section creates the storage account and containers for the policy documents. + +### 2. Prepare the storage account and containers + +For simplicity, all sources will use **Blob storage**, but each source is managed independently and can be updated or replaced as needed. + +First, create the **Storage Account** in Azure to host the Blob storage and the three containers for the policy documents. + +Run the following Azure CLI command to create the storage account. It uses the `foundryAccountName` environment variable because storage account names must be globally unique: + +```bash +az storage account create --name $foundryAccountName --resource-group $rg --location $location --sku Standard_LRS +``` + +Then, create the three containers in the storage account to store the policy documents: + +```bash +az storage container create --name global --account-name $foundryAccountName +az storage container create --name internal --account-name $foundryAccountName +az storage container create --name regional --account-name $foundryAccountName +``` + +Give appropriate permissions to the storage account and containers to ensure you can upload and manage the policy documents: + +```bash +userId=$(az ad signed-in-user show --query id -o tsv) +storageId=$(az storage account show \ + --name "$foundryAccountName" \ + --resource-group "$rg" \ + --query id -o tsv) + +az role assignment create \ + --assignee-object-id "$userId" \ + --assignee-principal-type User \ + --role "Storage Blob Data Contributor" \ + --scope "$storageId" +``` + +Finally, upload the policy documents into their respective containers. You can use the Azure CLI or Azure Storage Explorer. The `upload-batch` command uploads every file from each local directory: + +```bash +az storage blob upload-batch --account-name "$foundryAccountName" --destination global --source "$walkthroughHome/challenge-03/documents/global" --auth-mode login +az storage blob upload-batch --account-name "$foundryAccountName" --destination internal --source "$walkthroughHome/challenge-03/documents/internal" --auth-mode login +az storage blob upload-batch --account-name "$foundryAccountName" --destination regional --source "$walkthroughHome/challenge-03/documents/regional" --auth-mode login +``` + +To verify that the policy documents were uploaded successfully, open the **Storage Account** in the Azure portal and check the contents of each container: + +![Storage Account Contents](/challenges/images/storage-account-contents.png) + +Return to the **Foundry IQ** interface to continue adding and managing your policy sources. You can now add the policy sources from the Azure Blob storage containers you created. + +Select **Add Sources**, then choose **Azure Blob Storage** as the source type. + +![Add Azure Blob Storage Source](/challenges/images/add-azure-blob-storage-source.png) + +For each container, fill in the required details and click **Create** to link it as a policy source. + +The following example shows how to link the `global` container as a policy source: + +- **Name**: ks-global +- **Description**: Global and supranational rules and policies that must be always retrieved +- **Storage account** and **Container**: Specify the storage account name and the container name (`global`) where the policy documents are stored. +- **Authentication type**: API Key +- **Content extraction mode**: Minimal +- **Embedding model**: text-embedding-3-large +- **Chat completion model**: Not needed + +![Link Global Container](/challenges/images/link-global-container.png) + +The **Status** of the policy source displays **Creating** while it is being linked. During this process, **AI Search** begins indexing the policy documents in the container. To monitor indexing, open the **Search Service** resource and review the following areas: +- **Search management**: go into **Indexes** and **Indexers** to monitor the indexing process. +- **Agentic retrieval**: if you want to see the configuration created from **Foundry IQ**. + + +You will connect the `internal` and `regional` containers later in this guide so that you can compare the output and observe how the engine queries one or more sources. + + +### 3. Create the Regulatory Assessment Agent + +In the previous challenge, you created the `EvidenceEnrichmentAgent`. Now, create the `RegulatoryAssessmentAgent` to assess regulatory compliance using the policy sources in **Foundry IQ**. + +Because this is the second agent, follow the same **Microsoft Foundry** agent creation process you used for the `EvidenceEnrichmentAgent`. + +Relevant steps you should not miss include: + +- Define the agent's name as `RegulatoryAssessmentAgent` +- Set the model: `gpt-5.6-luna` +- Set instructions: find them under `walkthrough/challenge-03/regulatory-assessment-agent/instructions.md`. Review and validate content. +- Remove the `Web Search` tool, irrelevant for this agent. + +Next, add **Knowledge** by selecting **Add**, then **Connect to Foundry IQ**: + +![Connect to Foundry IQ](/challenges/images/connect-to-foundry-iq.png) + +Select the **Knowledge Base** you created (`kb-aml`), then select **Connect**: + +![Connect to Knowledge Base](/challenges/images/connect-to-knowledge-base.png) + +Finally, select **Save** to create the new agent and make it ready for use. + +Ensure that the agent has permission to access the connected **Foundry IQ** knowledge base by assigning the `Search Index Data Reader` role to the **Microsoft Foundry Project** managed identity: + +```bash +subscriptionId=$(az account show --query id -o tsv) + +projectId="/subscriptions/$subscriptionId/resourceGroups/$rg/providers/Microsoft.CognitiveServices/accounts/$foundryAccountName/projects/$foundryProjectName" + +projectPrincipalId=$(az resource show \ + --ids "$projectId" \ + --query identity.principalId \ + -o tsv) + +searchId=$(az search service list \ + --resource-group "$rg" \ + --query "[0].id" \ + -o tsv) + +az role assignment create \ + --assignee-object-id "$projectPrincipalId" \ + --assignee-principal-type ServicePrincipal \ + --role "Search Index Data Reader" \ + --scope "$searchId" +``` + +### 4. Test the agent with the global policy source + +Test the agent as you tested the `EvidenceEnrichmentAgent`. Use output from the previous agent as input. You can run the `EvidenceEnrichmentAgent` and save its output, or use the sample below. + +This is an example JSON output from the `EvidenceEnrichmentAgent`: + +```json +{ + "transaction_id": "TX-TEST-0001", + "original_transaction": { + "transaction_id": "TX-TEST-0001", + "originator_name": "James Carter", + "origin_account": "83D4B1F30", + "bank_origin": "0121", + "beneficiary_name": "Emily Foster", + "destination_account": "818CCA030", + "bank_destination": "29196", + "amount": 15000, + "currency": "EUR" + }, + "data_agent_response": { + "transaction_id": "TX-TEST-0001", + "match_found": true, + "assessment": "Historical laundering records were found for one or more accounts in the transaction.", + "origin_account": { + "originator_name": "James Carter", + "account": "83D4B1F30", + "bank_id": "0121", + "bank_name": "Israel Bank #35", + "country": "Israel", + "historical_participation": true, + "role": "Receiver", + "laundering_transaction_count": 3, + "laundering_attempt_count": 3, + "first_seen": "2026-09-02T10:03:00Z", + "last_seen": "2026-09-18T05:02:00Z", + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "evidence": [ + { + "pattern_txn_sk": 120259084422, + "attempt_id": 2502, + "pattern_type": "GATHER-SCATTER", + "step_in_attempt": 5, + "date_key": 20260918, + "txn_ts": "2026-09-18T05:02:00Z", + "from_bank_id": "0015", + "from_account": "84221F9F0", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 44947.3, + "receiving_currency": "Shekel", + "amount_paid": 44947.3, + "payment_currency": "Shekel", + "payment_format": "ACH", + "is_laundering": 1, + "id": "120259084422", + "partition_key": "2502" + }, + { + "pattern_txn_sk": 68719477003, + "attempt_id": 1385, + "pattern_type": "FAN-OUT", + "step_in_attempt": 1, + "date_key": 20260909, + "txn_ts": "2026-09-09T02:52:00Z", + "from_bank_id": "0015", + "from_account": "84221D110", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 23427.81, + "receiving_currency": "Shekel", + "amount_paid": 23427.81, + "payment_currency": "Shekel", + "payment_format": "ACH", + "is_laundering": 1, + "id": "68719477003", + "partition_key": "1385" + }, + { + "pattern_txn_sk": 8589934791, + "attempt_id": 150, + "pattern_type": "BIPARTITE", + "step_in_attempt": 13, + "date_key": 20260902, + "txn_ts": "2026-09-02T10:03:00Z", + "from_bank_id": "0220", + "from_account": "8000EB430", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 58403.73, + "receiving_currency": "Shekel", + "amount_paid": 58403.73, + "payment_currency": "Shekel", + "payment_format": "ACH", + "is_laundering": 1, + "id": "8589934791", + "partition_key": "150" + } + ] + }, + "destination_account": { + "beneficiary_name": "Emily Foster", + "account": "818CCA030", + "bank_id": "29196", + "bank_name": "Bank of Topeka", + "country": "Bangladesh", + "historical_participation": false, + "role": null, + "laundering_transaction_count": 0, + "laundering_attempt_count": 0, + "first_seen": null, + "last_seen": null, + "pattern_types": [], + "evidence": [] + } + }, + "historical_context": { + "accounts_with_history": 1, + "accounts_without_history": 1, + "highest_historical_context_level": 4, + "transaction_interpretation": "One account involved in the transaction has historical AML participation." + }, + "origin_account_enrichment": { + "historical_context_level": 4, + "historical_context_category": "Repeated Participation Across Multiple Patterns", + "participation_consistency": "Receiver Only", + "participation_frequency": "Repeated", + "pattern_diversity_count": 3, + "pattern_diversity_level": "High", + "days_since_last_seen": 10, + "recency_band": "Recent", + "investigator_interpretation": "The account shows repeated historical participation spanning multiple laundering patterns." + }, + "destination_account_enrichment": { + "historical_context_level": 0, + "historical_context_category": "No Historical AML Participation", + "participation_consistency": "None", + "participation_frequency": "None", + "pattern_diversity_count": 0, + "pattern_diversity_level": "None", + "days_since_last_seen": null, + "recency_band": "None", + "investigator_interpretation": "No historical AML participation was identified." + }, + "derived_features": { + "origin_has_history": true, + "destination_has_history": false, + "multiple_pattern_presence": true, + "recent_historical_activity": true, + "highest_historical_context_level": 4 + }, + "aml_regulatory_inputs": { + "historical_context_level": 4, + "pattern_diversity_count": 3, + "laundering_attempt_count": 3, + "recent_participation": true, + "historical_participation_role": "Receiver", + "historical_participation_detected": true + } +} +``` + +The output contains the enrichment details from the previous agent and additional context provided by the `global` policy source. Expand all evaluated files by selecting **+NN** at the bottom of the response: + +![Expand all files](/challenges/images/foundry-iq-global.png) + +Confirm that all files come from the `global` policy source by checking for `global` in each path. + +Next, add the internal and regional policy sources. + + +### 5. Add the internal and regional policy sources + +Return to **Knowledge** and select the `kb-aml` knowledge base you created. + +First, modify the **Retrieval instructions** to include the internal and regional policy sources after the global policies: + +```text +You have to always retrieve first the global policies and rules, then the internal rules and policies, and finally the regional rules and policies based on the transaction's bank origin and destination. Only the ones for these countries if documentation exists. +``` + +Then, add new **Azure Blob Storage** sources for the `internal` and `regional` containers. Repeat the process you followed for the `global` container, specifying the appropriate storage account, container, and authentication details for each. + +**Internal** + +- **Name**: ks-internal +- **Description**: Internal rules and policies +- **Storage account** and **Container**: Specify the storage account name and the container name (`internal`) where the policy documents are stored. +- **Authentication type**: API Key +- **Content extraction mode**: Minimal +- **Embedding model**: text-embedding-3-large +- **Chat completion model**: Not needed + +**Regional** + +- **Name**: ks-regional +- **Description**: Regional rules and policies +- **Storage account** and **Container**: Specify the storage account name and the container name (`regional`) where the policy documents are stored. +- **Authentication type**: API Key +- **Content extraction mode**: Minimal +- **Embedding model**: text-embedding-3-large +- **Chat completion model**: Not needed + + +Select **Save** to apply the changes to the knowledge base. Wait for all three sources to be indexed before testing the agent. Each source should display a status of **Active**. + +You do not need to modify the agent. The **Knowledge Base** provides a centralized source of domain knowledge, and the agent automatically uses updated information from all sources in the knowledge base. + +Finally, test the agent again and verify that its response and sources reflect the addition of the internal and regional policy sources. For example, if you use the JSON provided earlier, you should now see files for the transaction's origin and destination countries: + +![alt text showing the updated agent response with internal and regional policy sources](/challenges/images/foundry-iq-regional.png) + +You can also select **Traces** to review the agent's queries and how it retrieved the sources: + +![alt text showing the agent's trace of queries and source retrieval process](/challenges/images/foundry-iq-traces.png) + +## 🚀 Go Further + +Test a cross-border transaction for which one country has incomplete guidance. Compare broad retrieval queries with metadata-filtered queries and explain which approach produces the most defensible assessment. + +## 🛠️ Troubleshooting + +- **Relevant policies are not retrieved:** Check document ingestion, country metadata, index status, and the wording of the retrieval query. +- **Only one country is assessed:** Confirm that the agent derives and queries both the origin and destination countries. +- **Citations are missing:** Verify that citations are enabled and that the instructions require citations to survive into the structured output. +- **The agent invents a rule:** Strengthen the grounding instructions and require an explicit `insufficient_evidence` outcome when retrieval does not support a decision. +- **The next agent cannot parse the result:** Validate the response against the agreed assessment schema and remove explanatory text outside the structured result. + +## 🧠 Conclusion + +You have added a policy-grounded Regulatory Assessment Agent that evaluates global, internal, and country-specific AML requirements. Continue to [Challenge 4](solution-04.md) to generate an investigation report and orchestrate the first three agents. diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/aml-report-agent/instructions.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/aml-report-agent/instructions.md new file mode 100644 index 000000000..287c47c6c --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/aml-report-agent/instructions.md @@ -0,0 +1,245 @@ +## Purpose + +You are `AML Investigation Report Agent`. + +Transform the structured AML evidence, historical analysis, regulatory assessment, and reusable decision-support results into a professional Markdown report for AML analysts, compliance officers, auditors, investigators, and case reviewers. + +You are a presentation agent. Do not calculate, override, reinterpret, or infer regulatory scores, priority bands, compliance statuses, or final verdicts. + +Return Markdown only. Do not return JSON or expose internal reasoning. + +--- + +## Input + +The agent receives the complete JSON output from `RegulatoryAssessmentAgent`. It may include: + +- `transaction_id` +- `original_transaction` +- `data_agent_response` +- `historical_context` +- `origin_account_enrichment` +- `destination_account_enrichment` +- `derived_features` +- `aml_regulatory_inputs` +- `aml_regulatory_assessment` + +Decision-support values are provided at: + +- `aml_regulatory_assessment.decision_support.calculation_inputs` +- `aml_regulatory_assessment.decision_support.investigation_priority` +- `aml_regulatory_assessment.decision_support.workflow_classification` + +Use only information present in the input JSON. + +--- + +## Responsibilities + +- Generate a professional Markdown report. +- Preserve factual accuracy from the input JSON. +- Summarize the transaction and historical AML participation. +- Present historical evidence and regulatory rule evaluations. +- Highlight missing information and key regulatory findings. +- Present the supplied compliance status, priority score, priority band, final verdict, rationale, meaning, and disclaimer. +- Preserve the distinction between historical AML evidence and current wrongdoing. + +## Forbidden Actions + +- Do not recalculate any score, count, priority band, status, or verdict. +- Do not apply a verdict matrix or fallback rule. +- Do not alter `overall_compliance` or `decision_support` values. +- Do not declare that money laundering or fraud occurred. +- Do not state that any person or entity is guilty. +- Do not require a SAR, STR, or regulatory filing. +- Do not recommend blocking, freezing, rejecting, or approving a transaction. +- Do not invent regulations, evidence, missing facts, or policy interpretations. + +If required decision-support fields are missing, state that the regulatory assessment did not provide them. Do not derive replacements. + +--- + +## Report Structure + +```markdown +# AML Investigation Report + +## 1. Executive Summary + +## 2. Transaction Overview + +## 3. Historical AML Findings + +## 4. Historical Evidence Summary + +## 5. Regulatory Assessment Summary + +## 6. Missing Information + +## 7. Key Regulatory Findings + +## 8. Compliance and Decision-Support Summary + +## 9. Final Verdict +``` + +--- + +## Report Sections + +### 1. Executive Summary + +Include: + +- Transaction ID, amount, and currency. +- Originator and beneficiary name, account, bank, and country. +- Whether the transaction is cross-border. +- Whether either account has historical AML participation. +- Overall regulatory status. +- Supplied investigation priority and final verdict. + +### 2. Transaction Overview + +Use values from `original_transaction`, `data_agent_response`, and `aml_regulatory_assessment.regulatory_scope`. + +| Field | Value | +| --- | --- | +| Transaction ID | | +| Amount | | +| Currency | | +| Originator Name | | +| Origin Account | | +| Origin Bank ID | | +| Origin Bank Name | | +| Origin Country | | +| Beneficiary Name | | +| Destination Account | | +| Destination Bank ID | | +| Destination Bank Name | | +| Destination Country | | +| Cross-Border | Yes or No | + +### 3. Historical AML Findings + +Use `data_agent_response`, `origin_account_enrichment`, and `destination_account_enrichment`. + +| Account | Bank | Country | Historical AML Participation | Historical Role | Attempts | Transactions | Pattern Types | Context Level | +| --- | --- | --- | --- | --- | ---: | ---: | --- | ---: | + +Add a short factual interpretation. Historical participation is not proof of current wrongdoing. + +### 4. Historical Evidence Summary + +Display up to five evidence records per account, sorted by `txn_ts` descending. + +| Date | Account Role | Pattern | Attempt ID | Step | Counterparty Account | Counterparty Bank | Amount | Currency | Payment Format | +| --- | --- | --- | --- | ---: | --- | --- | ---: | --- | --- | + +If no evidence exists, state: `No historical evidence records were provided for this account.` + +### 5. Regulatory Assessment Summary + +Render `aml_regulatory_assessment.rule_evaluations` without changing any status. + +| Rule ID | Rule Name | Scope | Jurisdiction | Status | Applies | Summary | +| --- | --- | --- | --- | --- | --- | --- | + +Allowed supplied statuses are `COMPLIANT`, `POTENTIAL_GAP`, `NON_COMPLIANT`, `INSUFFICIENT_DATA`, and `NOT_APPLICABLE`. + +### 6. Missing Information + +Render `aml_regulatory_assessment.missing_data`. + +| Missing Field | Required For Rule | Reason | +| --- | --- | --- | + +If the array is empty, state: `No missing information was identified by the regulatory assessment.` + +### 7. Key Regulatory Findings + +Render `aml_regulatory_assessment.regulatory_interpretation.key_findings` as a concise bullet list. Do not add new findings. + +### 8. Compliance and Decision-Support Summary + +Render counts from `aml_regulatory_assessment.overall_compliance`: + +| Compliance Result | Count | +| --- | ---: | +| Compliant | | +| Potential Gap | | +| Insufficient Data | | +| Non-Compliant | | +| Not Applicable | | + +Then render the supplied reusable values: + +| Decision-Support Field | Value | +| --- | --- | +| Overall Regulatory Status | `overall_compliance.overall_status` | +| Historical Context Level | `decision_support.calculation_inputs.highest_historical_context_level` | +| Base Historical Context Score | `decision_support.investigation_priority.base_historical_context_score` | +| Potential Gap Points | `decision_support.investigation_priority.potential_gap_points` | +| Insufficient Data Points | `decision_support.investigation_priority.insufficient_data_points` | +| Non-Compliance Points | `decision_support.investigation_priority.non_compliance_points` | +| Raw Score | `decision_support.investigation_priority.raw_score` | +| Investigation Priority Score | `decision_support.investigation_priority.score` | +| Investigation Priority Band | `decision_support.investigation_priority.priority_band` | +| Matched Verdict Rule | `decision_support.workflow_classification.matched_rule_id` | + +Do not recompute or verify these values. Present them exactly as supplied. + +### 9. Final Verdict + +Use only `aml_regulatory_assessment.decision_support.workflow_classification` and `aml_regulatory_assessment.decision_support.investigation_priority`. + +```markdown +## 9. Final Verdict + +**Final Verdict:** `` + +**Overall Regulatory Status:** `` + +**Investigation Priority Score:** ` / ` + +**Investigation Priority Band:** `` + +**Matched Rule:** `` + +### Verdict Rationale + +- Render each supplied `aml_regulatory_assessment.decision_support.workflow_classification.rationale` item unchanged. + +### What This Verdict Means + +Render `aml_regulatory_assessment.decision_support.workflow_classification.meaning`. + +This workflow classification is not a legal conclusion and does not establish fraud, money laundering, or criminal activity. +``` + +If `is_legal_conclusion` or `establishes_money_laundering` is `true`, report the inconsistency as missing or invalid regulatory output. Do not repeat such a claim as a conclusion. + +--- + +## Report Tone + +- Professional. +- Neutral. +- Factual. +- Evidence-based. +- Audit-friendly. +- Concise but complete. + +Prefer terms such as `historical AML participation`, `regulatory relevance`, `potential regulatory gap`, `insufficient data`, `enhanced review`, `confirmed non-compliance`, and `available evidence`. + +Avoid terms such as `criminal`, `guilty`, `definitely laundering`, `fraudulent transaction`, `must be blocked`, and `must be reported`. + +--- + +## Final Rules + +- Return Markdown only. +- Use only fields present in the input JSON. +- Present regulatory calculations exactly as supplied. +- Never calculate a verdict or investigation priority score. +- Never silently repair inconsistent or missing regulatory values. +- Clearly state that the verdict is a workflow classification, not a legal conclusion. \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/azure.yaml b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/azure.yaml new file mode 100644 index 000000000..df201114c --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/azure.yaml @@ -0,0 +1,41 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +requiredVersions: + extensions: + azure.ai.agents: '>=1.0.0-beta.4' +name: fraud-intelligence-orchestration +services: + ai-project: + host: azure.ai.project + fraud-intelligence-orchestration: + host: azure.ai.agent + metadata: + tags: + - Agent Framework + - AI Agent Hosting + - Azure AI AgentServer + - Responses Protocol + - Streaming + project: src + language: python + uses: + - ai-project + kind: hosted + name: fraud-intelligence-orchestration + description: An orchestration of AI agents for fraud intelligence + protocols: + - protocol: responses + version: 2.0.0 + container: + resources: + cpu: '0.5' + memory: 1.0Gi + environmentVariables: + - name: AZURE_AI_AGENTSERVER_DISABLE_EXPERIMENTAL_WARNING + value: true + codeConfiguration: + runtime: python_3_14 + entryPoint: main.py + dependencyResolution: remote_build +infra: + provider: microsoft.foundry diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/run-agent.sh b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/run-agent.sh new file mode 100644 index 000000000..4f12bdc0b --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/run-agent.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +VENV_DIR="${HOME}/venvs/fraud-intelligence" + +python3 -m venv "$VENV_DIR" +source "$VENV_DIR/bin/activate" +trap 'deactivate 2>/dev/null || true' EXIT + +cd "$SCRIPT_DIR/src" + +python -m pip install -r requirements.txt +python main.py + +deactivate +trap - EXIT \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/src/.azdignore b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/src/.azdignore new file mode 100644 index 000000000..0c34457b0 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/src/.azdignore @@ -0,0 +1 @@ +.env.example diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/src/.dockerignore b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/src/.dockerignore new file mode 100644 index 000000000..0d6619bae --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/src/.dockerignore @@ -0,0 +1,7 @@ +.venv +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +.env \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/src/Dockerfile b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/src/Dockerfile new file mode 100644 index 000000000..eaffb94f1 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/src/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY . user_agent/ +WORKDIR /app/user_agent + +RUN if [ -f requirements.txt ]; then \ + pip install -r requirements.txt; \ + else \ + echo "No requirements.txt found"; \ + fi + +EXPOSE 8088 + +CMD ["python", "main.py"] \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/src/main.py b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/src/main.py new file mode 100644 index 000000000..cde9eccd2 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/src/main.py @@ -0,0 +1,84 @@ +# Copyright (c) Microsoft. All rights reserved. + +import os + +from agent_framework import AgentExecutor, WorkflowBuilder +from agent_framework.foundry import FoundryAgent +from agent_framework_foundry_hosting import ResponsesHostServer +from azure.ai.agentserver.core.tasks import set_resilient_tasks_enabled +from azure.identity import DefaultAzureCredential +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + + +def get_foundry_agent( + *, + project_endpoint: str, + credential: DefaultAzureCredential, + agent_name: str, + agent_version: str, +) -> FoundryAgent: + print(f"Getting Foundry agent: {agent_name} v{agent_version}") + return FoundryAgent( + project_endpoint=project_endpoint, + agent_name=agent_name, + agent_version=agent_version, + credential=credential, + ) + + +def main(): + set_resilient_tasks_enabled(True) + print("=== Fraud intelligence orchestration ===\n") + + project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + credential = DefaultAzureCredential() + + evidence_enrichment_agent = get_foundry_agent( + project_endpoint=project_endpoint, + credential=credential, + agent_name="evidenceenrichmentagent", + agent_version="2", + ) + + regulatory_assessment_agent = get_foundry_agent( + project_endpoint=project_endpoint, + credential=credential, + agent_name="RegulatoryAssessmentAgent", + agent_version="3", + ) + + aml_report_agent = get_foundry_agent( + project_endpoint=project_endpoint, + credential=credential, + agent_name="AMLReportAgent", + agent_version="2", + ) + + # Set the context mode to `last_agent` so that each agent only sees the output of the + # previous agent instead of the full conversation history + evidence_enrichment_executor = AgentExecutor(evidence_enrichment_agent, context_mode="last_agent") + regulatory_assessment_executor = AgentExecutor(regulatory_assessment_agent, context_mode="last_agent") + aml_report_executor = AgentExecutor(aml_report_agent, context_mode="last_agent") + + workflow_agent = ( + WorkflowBuilder( + start_executor=evidence_enrichment_executor, + # Limiting the output to only the final formatted result. + # If this is not set, all intermediate results will be included in the output. + output_from=[aml_report_executor], + ) + .add_edge(evidence_enrichment_executor, regulatory_assessment_executor) + .add_edge(regulatory_assessment_executor, aml_report_executor) + .build() + .as_agent() + ) + + server = ResponsesHostServer(workflow_agent) + server.run() + + +if __name__ == "__main__": + main() diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/src/requirements.txt b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/src/requirements.txt new file mode 100644 index 000000000..113cb1571 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/orchestration/src/requirements.txt @@ -0,0 +1,14 @@ +agent-framework-core==1.14.0 +agent-framework-foundry==1.11.0 +agent-framework-foundry-hosting==1.0.0b260813 +agent-framework-orchestrations==1.1.0 +azure-identity +azure-ai-projects>=2.3.0 +azure-cosmos>=4.9.0 +httpx>=0.28,<1 +opentelemetry-exporter-otlp-proto-grpc +python-dotenv +pydantic + +# debugpy enables local debugging of this agent with the Foundry Toolkit VS Code extension. +debugpy \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/solution-04.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/solution-04.md new file mode 100644 index 000000000..563365f87 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-04/solution-04.md @@ -0,0 +1,956 @@ +# Challenge 4 - Build and Orchestrate the Investigation + +[Previous challenge](solution-03.md) | **[Home](../README.md)** | [Next challenge](solution-05.md) + +## 🎯 Objective + +Build an **AML Report Agent**, compose it with the Evidence Enrichment and Regulatory Assessment agents using the **Microsoft Agent Framework**, and deploy the workflow as a hosted agent in Microsoft Foundry. + +## 🧭 Context and Background + +The first two agents produce evidence and a regulatory decision. The AML Report Agent converts those structured inputs into a consistent, audit-ready investigation report without changing the underlying facts or assessment. + +```mermaid +flowchart LR + TX[Transaction] --> ENRICH[Evidence Enrichment Agent] + ENRICH --> ASSESS[Regulatory Assessment Agent] + ASSESS --> REPORT[AML Report Agent] + REPORT --> OUTPUT[Investigation report] + + subgraph HOSTED[Foundry hosted agent] + ENRICH + ASSESS + REPORT + end +``` + +The orchestration owns sequencing, state transfer, failure handling, and the final response. Each specialist agent retains a narrow responsibility. + +## ✅ Tasks + +Before starting, source `baseenv` and `hackenv` to load the environment variables used by this challenge. + +```bash +source baseenv +source hackenv +``` + +### 1. Create the AML Report Agent + +The AML report agent is responsible for generating a comprehensive investigation report based on the assessments provided by the evidence enrichment and regulatory assessment agents. It ensures that all findings are documented in a structured and audit-ready format. + +The report must preserve source identifiers so an analyst can trace each finding back to its evidence or policy. It must also preserve the regulatory status, avoid adding facts, and distinguish missing information from confirmed findings. + +This is the third agent you will build, so follow the same creation and configuration process used for the previous agents. Configure the following settings: + +- Define the agent's name as `AMLReportAgent` +- Set the model: `gpt-5.6-luna` +- Set instructions: find them under `walkthrough/challenge-04/aml-report-agent/instructions.md`. Review and validate content. +- Remove the `Web Search` tool, irrelevant for this agent. + +This agent does not require any tools, so skip the tool configuration step. Select **Save** to create the agent and make it ready for use. + +### 2. Test the AML Report Agent + +Test the AML Report Agent using the same approach as for the previous agents. Verify that it generates a comprehensive, structured investigation report based on the assessments from the Evidence Enrichment and Regulatory Assessment agents. + +If you saved the output from the `RegulatoryAssessmentAgent` test, provide it as input to the `AMLReportAgent` to simulate a complete investigation workflow. Otherwise, use the following JSON: + +```json +{ + "transaction_id": "TX-TEST-0001", + "original_transaction": { + "transaction_id": "TX-TEST-0001", + "originator_name": "James Carter", + "origin_account": "83D4B1F30", + "bank_origin": "0121", + "beneficiary_name": "Emily Foster", + "destination_account": "818CCA030", + "bank_destination": "29196", + "amount": 15000, + "currency": "EUR" + }, + "data_agent_response": { + "transaction_id": "TX-TEST-0001", + "match_found": true, + "assessment": "Historical laundering records were found for one or more accounts in the transaction.", + "origin_account": { + "originator_name": "James Carter", + "account": "83D4B1F30", + "bank_id": "0121", + "bank_name": "Israel Bank #35", + "country": "Israel", + "historical_participation": true, + "role": "Receiver", + "laundering_transaction_count": 3, + "laundering_attempt_count": 3, + "first_seen": "2026-09-02T10:03:00Z", + "last_seen": "2026-09-18T05:02:00Z", + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "evidence": [ + { + "pattern_txn_sk": 120259084422, + "attempt_id": 2502, + "pattern_type": "GATHER-SCATTER", + "step_in_attempt": 5, + "date_key": 20260918, + "txn_ts": "2026-09-18T05:02:00Z", + "from_bank_id": "0015", + "from_account": "84221F9F0", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 44947.3, + "receiving_currency": "Shekel", + "amount_paid": 44947.3, + "payment_currency": "Shekel", + "payment_format": "ACH", + "is_laundering": 1, + "id": "120259084422", + "partition_key": "2502" + }, + { + "pattern_txn_sk": 68719477003, + "attempt_id": 1385, + "pattern_type": "FAN-OUT", + "step_in_attempt": 1, + "date_key": 20260909, + "txn_ts": "2026-09-09T02:52:00Z", + "from_bank_id": "0015", + "from_account": "84221D110", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 23427.81, + "receiving_currency": "Shekel", + "amount_paid": 23427.81, + "payment_currency": "Shekel", + "payment_format": "ACH", + "is_laundering": 1, + "id": "68719477003", + "partition_key": "1385" + }, + { + "pattern_txn_sk": 8589934791, + "attempt_id": 150, + "pattern_type": "BIPARTITE", + "step_in_attempt": 13, + "date_key": 20260902, + "txn_ts": "2026-09-02T10:03:00Z", + "from_bank_id": "0220", + "from_account": "8000EB430", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 58403.73, + "receiving_currency": "Shekel", + "amount_paid": 58403.73, + "payment_currency": "Shekel", + "payment_format": "ACH", + "is_laundering": 1, + "id": "8589934791", + "partition_key": "150" + } + ] + }, + "destination_account": { + "beneficiary_name": "Emily Foster", + "account": "818CCA030", + "bank_id": "29196", + "bank_name": "Bank of Topeka", + "country": "Bangladesh", + "historical_participation": false, + "role": null, + "laundering_transaction_count": 0, + "laundering_attempt_count": 0, + "first_seen": null, + "last_seen": null, + "pattern_types": [], + "evidence": [] + } + }, + "historical_context": { + "accounts_with_history": 1, + "accounts_without_history": 1, + "highest_historical_context_level": 4, + "transaction_interpretation": "One account involved in the transaction has historical AML participation." + }, + "origin_account_enrichment": { + "historical_context_level": 4, + "historical_context_category": "Repeated Participation Across Multiple Patterns", + "participation_consistency": "Receiver Only", + "participation_frequency": "Repeated", + "pattern_diversity_count": 3, + "pattern_diversity_level": "High", + "days_since_last_seen": 10, + "recency_band": "Recent", + "investigator_interpretation": "The account shows repeated historical participation spanning multiple laundering patterns." + }, + "destination_account_enrichment": { + "historical_context_level": 0, + "historical_context_category": "No Historical AML Participation", + "participation_consistency": "None", + "participation_frequency": "None", + "pattern_diversity_count": 0, + "pattern_diversity_level": "None", + "days_since_last_seen": null, + "recency_band": "None", + "investigator_interpretation": "No historical AML participation was identified." + }, + "derived_features": { + "origin_has_history": true, + "destination_has_history": false, + "multiple_pattern_presence": true, + "recent_historical_activity": true, + "highest_historical_context_level": 4 + }, + "aml_regulatory_inputs": { + "historical_context_level": 4, + "pattern_diversity_count": 3, + "laundering_attempt_count": 3, + "recent_participation": true, + "historical_participation_role": "Receiver", + "historical_participation_detected": true + }, + "aml_regulatory_assessment": { + "assessment_source": "kb-aml", + "assessment_type": "AML regulation and policy compliance validation", + "regulatory_scope": { + "global_rules_evaluated": true, + "supranational_rules_evaluated": true, + "origin_country_rules_evaluated": true, + "destination_country_rules_evaluated": true, + "cross_border_rules_evaluated": true, + "historical_aml_context_rules_evaluated": true, + "origin_country": "Israel", + "destination_country": "Bangladesh", + "is_cross_border": true + }, + "retrieved_policy_sets": [ + { + "policy_set_id": "GLB-STD-001", + "policy_set_name": "Global AML/CFT Standards - Consolidated", + "scope": "GLOBAL", + "jurisdiction": null, + "summary": "Risk-based AML controls, customer due diligence, enhanced due diligence, wire-transfer transparency, monitoring, reporting and record keeping.", + "source_reference": "GLB-STD-001, pa[[1]]†source】" + }, + { + "policy_set_id": "EU-GL-002", + "policy_set_name": "EU ML/TF Risk Factors Guidelines", + "scope": "SUPRANATIONAL", + "jurisdiction": "European Union", + "summary": "Risk-factor and enhanced-due-diligence guidance retrieved as supranational material; direct applicability to these non-EU jurisdictions is not established by the input.", + "source_reference": "EU[[2]]†source】" + }, + { + "policy_set_id": "IL-REG-016", + "policy_set_name": "Israel - Prohibition on Money Laundering", + "scope": "ORIGIN_COUNTRY", + "jurisdiction": "Israel", + "summary": "Identification, beneficiary and controlling-shareholder declarations, cross-border reporting and suspicious-activity duties.", + "source_reference": "IL-REG-016, pa[[3]]†source】" + }, + { + "policy_set_id": "BD-REG-017", + "policy_set_name": "Bangladesh - Money Laundering Prevention", + "scope": "DESTINATION_COUNTRY", + "jurisdiction": "Bangladesh", + "summary": "Customer identification, suspicious-activity reporting, outward-remittance purpose documentation and foreign-exchange records.", + "source_reference": "BD-REG-017, pa[[4]]†source】" + }, + { + "policy_set_id": "GLB-STD-001-CB", + "policy_set_name": "Global Cross-Border Wire Transfer Controls", + "scope": "CROSS_BORDER", + "jurisdiction": "Israel to Bangladesh", + "summary": "Cross-border transfers require accurate originator and beneficiary information and retention of required information through the payment chain.", + "source_reference": "GLB-STD-001, wire-transfer [[5]]†source】" + }, + { + "policy_set_id": "OPS-GEO-005", + "policy_set_name": "Jurisdiction Risk Matrix and Corridor Rules", + "scope": "CROSS_BORDER", + "jurisdiction": "Israel to Bangladesh", + "summary": "Retrieved country ratings are Israel 2 and Bangladesh 3; the higher rating is relevant to corridor review.", + "source_reference": "OPS-[[6]]†source】" + }, + { + "policy_set_id": "OPS-RULE-021-HIST", + "policy_set_name": "Payment-Level Detection Rules - Prior Involvement", + "scope": "HISTORICAL_AML_CONTEXT", + "jurisdiction": null, + "summary": "Prior reported or otherwise documented AML involvement is a specific historical-context evaluation family.", + "source_reference": "OPS-RULE-021, HIST[[7]]†source】" + }, + { + "policy_set_id": "GLB-TYP-015", + "policy_set_name": "Typology Casebook and Detection Scenarios", + "scope": "HISTORICAL_AML_CONTEXT", + "jurisdiction": null, + "summary": "Multiple typologies and repeated historical participation are relevant indicators, but a single indicator is not evidence of laundering.", + "source_reference": "GLB-[[8]]†source】" + } + ], + "rule_evaluations": [ + { + "rule_id": "GLB-STD-001-RBA", + "rule_name": "Risk-based assessment of geography and transaction profile", + "policy_set_name": "Global AML/CFT Standards - Consolidated", + "scope": "GLOBAL", + "jurisdiction": null, + "rule_summary": "Obliged entities shall assess customer, product, channel and origin/destination geography risks and apply proportionate controls.", + "applies_to_transaction": true, + "compliance_status": "COMPLIANT", + "validation_result": { + "passed": true, + "failed": false, + "missing_required_data": false + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "The input contains origin and destination countries, transaction value and currency, bank identifiers, and substantial historical AML context. These fields support risk-based assessment. They do not establish that all required mitigating controls were completed.", + "kb_source_reference": "GLB-[[9]]†source】" + }, + { + "rule_id": "GLB-STD-001-CDD", + "rule_name": "Customer due diligence and ongoing due diligence", + "policy_set_name": "Global AML/CFT Standards - Consolidated", + "scope": "GLOBAL", + "jurisdiction": null, + "rule_summary": "CDD includes identity verification, beneficial-owner identification, purpose and intended-nature information, and ongoing scrutiny against customer profile and source of funds or wealth.", + "applies_to_transaction": true, + "compliance_status": "INSUFFICIENT_DATA", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "Names and account identifiers are present, but the input does not establish identity verification, beneficial-owner information, transaction purpose, source of funds, source of wealth or customer-profile consistency.", + "kb_source_reference": "GLB-[[10]]†source】" + }, + { + "rule_id": "GLB-STD-001-EDD", + "rule_name": "Enhanced due diligence for complex or unusually large transactions", + "policy_set_name": "Global AML/CFT Standards - Consolidated", + "scope": "GLOBAL", + "jurisdiction": null, + "rule_summary": "Enhanced measures apply to complex or unusually large transactions without an apparent economic or lawful purpose and require additional customer, ownership, purpose and source-of-funds information.", + "applies_to_transaction": true, + "compliance_status": "POTENTIAL_GAP", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "The transaction is cross-border and one account has recent repeated participation across three historical AML patterns. The input does not show whether enhanced due diligence, senior approval, source-of-funds corroboration or purpose validation occurred.", + "kb_source_reference": "GLB-[[11]]†source】" + }, + { + "rule_id": "GLB-STD-001-WIRE", + "rule_name": "Cross-border wire-transfer information completeness", + "policy_set_name": "Global Cross-Border Wire Transfer Controls", + "scope": "CROSS_BORDER", + "jurisdiction": "Israel to Bangladesh", + "rule_summary": "Cross-border transfers must include originator name and account or unique reference, beneficiary name and account or unique reference, plus originator address, national ID, or date and place of birth.", + "applies_to_transaction": true, + "compliance_status": "POTENTIAL_GAP", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "Originator and beneficiary names and account numbers are supplied. Originator address, national identification number, or date and place of birth are absent, and the input does not confirm that required data travelled with the payment.", + "kb_source_reference": "GLB-[[12]]†source】" + }, + { + "rule_id": "GLB-STD-001-MON", + "rule_name": "Transaction monitoring and suspicious-activity escalation", + "policy_set_name": "Global AML/CFT Standards - Consolidated", + "scope": "GLOBAL", + "jurisdiction": null, + "rule_summary": "Institutions must monitor transactions, assess suspicious indicators, escalate promptly and report without delay where suspicion or reasonable grounds to suspect arise.", + "applies_to_transaction": true, + "compliance_status": "POTENTIAL_GAP", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "The enriched data supplies significant monitoring indicators and historical records, but does not state whether this transaction was reviewed, escalated, reported, or documented by the compliance function. The evidence supports a potential control gap, not a finding of non-compliance.", + "kb_source_reference": "GLB-[[13]]†source】" + }, + { + "rule_id": "IL-REG-016-ID", + "rule_name": "Origin-jurisdiction identification and beneficiary declaration", + "policy_set_name": "Israel - Prohibition on Money Laundering", + "scope": "ORIGIN_COUNTRY", + "jurisdiction": "Israel", + "rule_summary": "Identification, official verification, beneficiary declaration and controlling-shareholder declaration are required as specified by the applicable sectoral order.", + "applies_to_transaction": true, + "compliance_status": "INSUFFICIENT_DATA", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "The origin country is Israel and the originating bank is identified. The input does not contain verification records, beneficiary declarations, controlling-shareholder information, applicable sectoral order, or evidence of compliance with any cross-border reporting threshold.", + "kb_source_reference": "IL-[[14]]†source】" + }, + { + "rule_id": "BD-REG-017-REM", + "rule_name": "Destination-jurisdiction remittance purpose and record requirements", + "policy_set_name": "Bangladesh - Money Laundering Prevention", + "scope": "DESTINATION_COUNTRY", + "jurisdiction": "Bangladesh", + "rule_summary": "Outward remittances require an authorised purpose and supporting documentation; foreign-exchange transaction records must be retained.", + "applies_to_transaction": true, + "compliance_status": "INSUFFICIENT_DATA", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "The destination account is in Bangladesh, but the input does not provide an authorised purpose code, supporting documentation, licensed remittance channel, sender relationship, or foreign-exchange record.", + "kb_source_reference": "BD-[[15]]†source】" + }, + { + "rule_id": "OPS-GEO-005-CORRIDOR", + "rule_name": "Cross-border corridor risk review", + "policy_set_name": "Jurisdiction Risk Matrix and Corridor Rules", + "scope": "CROSS_BORDER", + "jurisdiction": "Israel to Bangladesh", + "rule_summary": "The corridor assessment considers both country ratings and applies the higher rating to the transaction.", + "applies_to_transaction": true, + "compliance_status": "COMPLIANT", + "validation_result": { + "passed": true, + "failed": false, + "missing_required_data": false + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "The countries differ, so cross-border review applies. The retrieved matrix rates Israel 2 and Bangladesh 3; the assessment records the higher Bangladesh rating. No call-for-action or sanctions-programme designation was retrieved for either country.", + "kb_source_reference": "OPS-[[16]]†source】" + }, + { + "rule_id": "OPS-RULE-021-HIST-01", + "rule_name": "Prior AML involvement and historical context evaluation", + "policy_set_name": "Payment-Level Detection Rules - Prior Involvement", + "scope": "HISTORICAL_AML_CONTEXT", + "jurisdiction": null, + "rule_summary": "Accounts previously identified in AML activity are subject to a dedicated historical-involvement evaluation family.", + "applies_to_transaction": true, + "compliance_status": "COMPLIANT", + "validation_result": { + "passed": true, + "failed": false, + "missing_required_data": false + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "The origin account has three recorded laundering transactions and attempts, three pattern types, historical context level 4, and recent activity. The historical-context rule therefore applies and the required historical indicators are present.", + "kb_source_reference": "OPS-RULE-021 HIST[[17]]†source】" + }, + { + "rule_id": "GLB-TYP-015-HIGH", + "rule_name": "Multiple historical typology indicators", + "policy_set_name": "Typology Casebook and Detection Scenarios", + "scope": "HISTORICAL_AML_CONTEXT", + "jurisdiction": null, + "rule_summary": "Co-occurring typology indicators have enhanced analytical relevance, while a single indicator alone is not evidence of laundering.", + "applies_to_transaction": true, + "compliance_status": "COMPLIANT", + "validation_result": { + "passed": true, + "failed": false, + "missing_required_data": false + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 4, + "pattern_types": [ + "BIPARTITE", + "FAN-OUT", + "GATHER-SCATTER" + ], + "recent_historical_activity": true + }, + "interpretation": "The origin account appears across three historical pattern types and has recent repeated participation. These are regulatory monitoring indicators, not a determination that the current transaction constitutes laundering.", + "kb_source_reference": "GLB-[[18]]†source】" + } + ], + "overall_compliance": { + "overall_status": "POTENTIAL_GAP", + "non_compliant_rule_count": 0, + "potential_gap_rule_count": 3, + "insufficient_data_rule_count": 3, + "not_applicable_rule_count": 0, + "compliant_rule_count": 4 + }, + "decision_support": { + "calculation_version": "1.0", + "calculation_inputs": { + "highest_historical_context_level": 4, + "non_compliant_rule_count": 0, + "potential_gap_rule_count": 3, + "insufficient_data_rule_count": 3, + "not_applicable_rule_count": 0, + "compliant_rule_count": 4 + }, + "investigation_priority": { + "base_historical_context_score": 50, + "potential_gap_points": 30, + "insufficient_data_points": 15, + "non_compliance_points": 0, + "raw_score": 95, + "score": 95, + "maximum_score": 100, + "priority_band": "Critical" + }, + "workflow_classification": { + "final_verdict": "ENHANCED REVIEW REQUIRED", + "matched_rule_id": "LEVEL_C", + "rationale": [ + "The highest historical context level is 4.", + "Three potential regulatory gaps were identified.", + "Three applicable rules could not be fully assessed because required data was absent.", + "No retrieved rule was assessed as non-compliant." + ], + "meaning": "The transaction has substantial historical AML relevance and unresolved regulatory validation gaps. The classification is a workflow result and does not establish a legal breach or money laundering.", + "is_legal_conclusion": false, + "establishes_money_laundering": false + } + }, + "regulatory_interpretation": { + "summary": "The transaction is regulatory-relevant because it is cross-border from Israel to Bangladesh and the originating account has recent, repeated historical AML participation across three pattern types. The retrieved rules do not support a finding of non-compliance on the available evidence. However, CDD, enhanced-due-diligence, wire-transfer, monitoring, Israeli reporting, and Bangladesh remittance-purpose fields are incomplete or unverified.", + "key_findings": [ + "The origin account has historical AML participation with three recorded laundering transactions and attempts.", + "The origin account has historical participation across BIPARTITE, FAN-OUT and GATHER-SCATTER patterns.", + "The destination account has no historical AML participation in the supplied enrichment.", + "The transaction is cross-border because the origin and destination countries differ.", + "The retrieved jurisdiction matrix rates Israel 2 and Bangladesh 3; no sanctions or call-for-action designation was retrieved for either country.", + "Originator and beneficiary names and account identifiers are present, but address or equivalent originator identification data is absent.", + "CDD, source-of-funds, transaction-purpose, sanctions-screening, enhanced-review and reporting-status evidence is absent." + ], + "regulatory_relevance": "The available evidence supports enhanced regulatory scrutiny and leaves multiple control-validation gaps. It does not establish fraud, money laundering, suspicious activity, or a mandatory operational outcome. Sources: GLB-[[19]]†source】, IL-[[20]]†source】, BD-[[21]]†source】." + }, + "missing_data": [ + { + "field": "identity_verification_status", + "required_for_rule": "GLB-STD-001-CDD and IL-REG-016-ID", + "reason": "Names and account identifiers do not demonstrate verification using reliable independent documents or electronic verification." + }, + { + "field": "beneficial_owner_and_controlling_shareholder_information", + "required_for_rule": "GLB-STD-001-CDD and IL-REG-016-ID", + "reason": "The input does not identify or verify beneficial ownership or controlling-shareholder declarations." + }, + { + "field": "transaction_purpose_and_intended_nature", + "required_for_rule": "GLB-STD-001-CDD and BD-REG-017-REM", + "reason": "No purpose code, economic rationale or intended-nature information is supplied." + }, + { + "field": "source_of_funds_and_source_of_wealth", + "required_for_rule": "GLB-STD-001-CDD and GLB-STD-001-EDD", + "reason": "The input does not contain source-of-funds or source-of-wealth information or corroboration." + }, + { + "field": "originator_address_or_national_id_or_date_and_place_of_birth", + "required_for_rule": "GLB-STD-001-WIRE", + "reason": "The retrieved cross-border wire rule requires one of these originator data elements, which is absent." + }, + { + "field": "wire_transfer_information_propagation_status", + "required_for_rule": "GLB-STD-001-WIRE", + "reason": "The input does not confirm that required originator and beneficiary information remained with the payment through the chain." + }, + { + "field": "enhanced_due_diligence_and_senior_approval_status", + "required_for_rule": "GLB-STD-001-EDD", + "reason": "The input does not show whether enhanced review or senior approval was performed in light of the historical context and transaction profile." + }, + { + "field": "transaction_monitoring_review_and_escalation_status", + "required_for_rule": "GLB-STD-001-MON", + "reason": "No compliance review, escalation, report filing, or documented disposition is supplied." + }, + { + "field": "israel_sectoral_order_and_cross_border_reporting_status", + "required_for_rule": "IL-REG-016-ID", + "reason": "The applicable sectoral order and any required cross-border report status are not provided." + }, + { + "field": "bangladesh_authorised_remittance_purpose_code_and_supporting_documentation", + "required_for_rule": "BD-REG-017-REM", + "reason": "The destination-country policy requires an authorised purpose and supporting documentation for outward remittance records." + }, + { + "field": "sanctions_screening_result", + "required_for_rule": "Global sanctions-screening controls", + "reason": "No sanctions-screening result is present in the input. No specific sanctions match can be determined." + } + ], + "downstream_inputs": { + "has_regulatory_non_compliance": false, + "has_potential_regulatory_gap": true, + "requires_case_recommendation_review": true, + "highest_historical_context_level": 4, + "rules_triggered": [ + "GLB-STD-001-RBA", + "GLB-STD-001-CDD", + "GLB-STD-001-EDD", + "GLB-STD-001-WIRE", + "GLB-STD-001-MON", + "IL-REG-016-ID", + "BD-REG-017-REM", + "OPS-GEO-005-CORRIDOR", + "OPS-RULE-021-HIST-01", + "GLB-TYP-015-HIGH" + ], + "countries_evaluated": [ + "Israel", + "Bangladesh" + ], + "policy_scopes_evaluated": [ + "GLOBAL", + "SUPRANATIONAL", + "ORIGIN_COUNTRY", + "DESTINATION_COUNTRY", + "CROSS_BORDER", + "HISTORICAL_AML_CONTEXT" + ] + } + } +} +``` + +The output is a Markdown report summarizing the regulatory compliance gaps and potential issues identified from the two previous agents' input. + +### 3. Review the Agent Framework Orchestration + +Several frameworks are available for orchestrating agents, each with its own approach to communication, task delegation, and data flow. This challenge uses [Agent Framework](https://learn.microsoft.com/en-us/agent-framework/) to coordinate multiple agents. + +First, run the orchestration locally and test it with the `Agent inspector` tool provided by the **Foundry Toolkit** for Visual Studio Code. + +The source code is in the `orchestration` directory under `challenge-04`. Review these key files: + +- `src/main.py`: The main entry point for the orchestration logic. That is where we will initialize and coordinate the various agents involved in the process. +- `azure.yaml`: Configuration file for the agent that is required during the phase of deploying the agent as **Hosted Agent** in **Microsoft Foundry**. + +Ensure that the agents are defined correctly in the `main.py` file and that their names and versions match those running in the Foundry environment. Open the **Agents** section in the portal to verify these values. + +Those names and versions should be set correctly in the `main.py` file: + +![Agents versions](/challenges/images/agents-versions.png) + +### 4. Configure and Run the Orchestration Locally + +Create an `.env` file in the `src` folder containing the **FOUNDRY_PROJECT_ENDPOINT**. The local orchestration authenticates using the identity signed in to Azure through `az login`. Run the following command to create the file: + +```bash +echo "FOUNDRY_PROJECT_ENDPOINT=https://$foundryAccountName.services.ai.azure.com/api/projects/$foundryProjectName" > $walkthroughHome/challenge-04/orchestration/src/.env +``` + +Validate the `.env` file to ensure that the **FOUNDRY_PROJECT_ENDPOINT** is set correctly: + +```bash +cat $walkthroughHome/challenge-04/orchestration/src/.env +``` + +After setting the endpoint, run the agent orchestration locally using the provided script: + +```bash +$walkthroughHome/challenge-04/orchestration/run-agent.sh +``` + +The script will set up a virtual environment, install the required dependencies, and then run the main orchestration logic. This allows you to test the entire agent workflow locally before deploying it to the hosted environment. + +In the logs, you should see the initialization of each agent: + +![Agents initialization](/challenges/images/agents-initialization.png) + + +The final trace shows the port on which the agent is listening: + +![Final trace showing the port the agent is listening on](/challenges/images/orchestration-started.png) + +To test the orchestration locally, select the **Foundry Toolkit** icon in Visual Studio Code and use the **Agent inspector** tool to interact with the running agents: + +![Agent inspector tool in Foundry Toolkit](/challenges/images/agent-inspector.png) + +Then use the initial JSON request from Challenge 2 to interact with the agents: +```json +{ + "transaction_id": "TX-TEST-0001", + "originator_name": "James Carter", + "origin_account": "83D4B1F30", + "bank_origin": "0121", + "beneficiary_name": "Emily Foster", + "destination_account": "818CCA030", + "bank_destination": "29196", + "amount": 15000, + "currency": "EUR" +} +``` + +Press `Enter` to send the request. Processing may take some time because all three agents run sequentially. + + +The result should be a Markdown report containing the transaction details and the analysis performed by the agents. + +### 5. Deploy to Microsoft Foundry + +Before deploying, ensure that you are signed in to your Azure account. Use the Azure extension in Visual Studio Code for this process. Select the **Azure** icon in the left sidebar to open the extension panel: + +![Azure Extension new account](/challenges/images/azure-extension-new-account.png) + +Select **Sign in with new account** and enter the credentials for the Azure account provided for the lab. + +Before proceeding, confirm that the correct Azure subscription is selected. Open **View** > **Command Palette**, then search for `Azure: Select Subscriptions` and choose the appropriate subscription: + +![Command Palette](/challenges/images/command-palette.png) + +Then: + +![Select Subscription in Azure](/challenges/images/select-azure-subscription.png) + +Return to the **Foundry Toolkit** extension and set your **Foundry project** as the default. Under **My resources**, select **Set Foundry project**, then choose the project used for this lab. + +After setting the default, you can manage your Foundry project resources and deploy the orchestration directly from Visual Studio Code. + +There are multiple ways to deploy your orchestration to Microsoft Foundry. You can use the `azd` command-line tool or deploy directly from the **Foundry Toolkit** extension. + +**Important Note:** due to limitations in how the extension manages source code, move the orchestration source and the `azure.yaml` agent definition to the root of the project directory before deploying. Run the following command: + +```bash +cp -Rf \ + "$walkthroughHome/challenge-04/orchestration/src" \ + "$walkthroughHome/challenge-04/orchestration/azure.yaml" \ + "$rootHome/" +``` + +Use the latter method to deploy directly from the **Foundry Toolkit** extension. Under **Developer Tools**, expand the **Build** section, then select **Deploy to Microsoft Foundry**. + +Follow these steps to deploy your orchestration. Ensure you use: +- **Deployment method**: Code +- **Package mode**: Remote +- **Deploy to**: New agent, as it is the first deployment of this orchestration + +![Deploy to Microsoft Foundry](/challenges/images/deploy-to-microsoft-foundry.png) + +Finally, review the deployment options before confirming the deployment: + +![Review deployment options](/challenges/images/review-deployment-options.png) + +Track the deployment progress in the **Output** panel in Visual Studio Code by selecting **Foundry Toolkit** as the output source: + +![Deployment progress in Output panel](/challenges/images/deployment-progress-in-output-panel.png) + +Once the deployment is complete, you can run a test using the **Hosted Agent Playground**: + +![Hosted Agent Playground](/challenges/images/hosted-agent-playground.png) + +The new hosted agent also appears under **Agents** in the **Microsoft Foundry** portal, where you can interact with it and test its functionality: + +![Microsoft Foundry Agents](/challenges/images/microsoft-foundry-hosted-agents.png) + +Test it from the playground as well: + +![Try from the playground](/challenges/images/try-from-the-playground.png) + +### 6. Review Traces + +Challenge 6 explores this topic in depth. For now, take a preliminary look at the traces generated by your orchestration. + +In the agent playground, select **Traces**: + +![Traces in the agent's playground](/challenges/images/traces-in-agents-playground.png) + +Open the last trace to inspect the detailed execution flow of your orchestration: + +![Detailed execution flow of the last trace](/challenges/images/detailed-execution-flow-of-last-trace.png) + +We will explore tracing and metrics in more depth in Challenge 6. For now, this preliminary view is enough to confirm that the three agents ran in the expected sequence. + +## 🚀 Go Further + +Add idempotent case identifiers and checkpointing so a transient failure can resume without repeating completed investigation stages. + +## 🛠️ Troubleshooting + +- **A remote agent cannot be invoked:** Verify the project endpoint, agent name and version, managed identity, and role assignments. +- **A handoff fails validation:** Compare the producing agent's output with the consuming schema and inspect the raw response in the trace. +- **The report changes the decision:** Ensure the report agent receives the assessment as authoritative and has no instruction to reassess it. +- **Local authentication fails:** Sign in with the lab identity and confirm that `DefaultAzureCredential` selects the intended credential. +- **Hosted deployment fails:** Review the build logs, entry point, dependency versions, environment variables, and hosted-agent health status. + +## 🧠 Conclusion + +You have composed evidence enrichment, regulatory assessment, and reporting into a deployable investigation workflow. Continue to [Challenge 5](solution-05.md) to govern model and MCP traffic and add operational alerting. \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/alert-manager-agent/instructions.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/alert-manager-agent/instructions.md new file mode 100644 index 000000000..7a5965572 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/alert-manager-agent/instructions.md @@ -0,0 +1,334 @@ +## Purpose + +You are `AML Alert Manager Agent`. + +Your responsibility is to evaluate the supplied regulatory status and create an alert through the Fraud Alert Manager MCP when the alert threshold is met. + +You are an alert-routing agent. You do not perform AML analysis, calculate scores, change verdicts, or determine regulatory compliance. + +--- + +## Input + +The input may include: + +- `transaction_id` +- `original_transaction` +- `data_agent_response` +- `historical_context` +- `origin_account_enrichment` +- `destination_account_enrichment` +- `derived_features` +- `aml_regulatory_inputs` +- `aml_regulatory_assessment` + +The fields used to decide whether to create an alert and to build the MCP request are: + +- `aml_regulatory_assessment.overall_compliance` +- `aml_regulatory_assessment.decision_support` +- `aml_regulatory_assessment.regulatory_interpretation` +- `original_transaction` + +Use only information present in the input JSON. + +--- + +## Alert Threshold + +Read the status from: + +`aml_regulatory_assessment.overall_compliance.overall_status` + +Apply this rule exactly: + +| Overall status | Action | +| --- | --- | +| `COMPLIANT` | Do not create an alert | +| Any other supplied value | Create an alert | + +This includes `POTENTIAL_GAP`, `INSUFFICIENT_DATA`, and `NON_COMPLIANT`. + +Do not use the investigation priority score, priority band, historical context level, final verdict, individual rule statuses, or your own judgment as an alternative threshold. + +If `overall_status` is missing, null, empty, or not a string, do not invoke the MCP. Return the invalid-input result defined below. + +--- + +## Required Workflow + +Follow these steps in order: + +1. Validate that the input is a JSON object. +2. Read `aml_regulatory_assessment.overall_compliance.overall_status` without modifying it. +3. If the status is `COMPLIANT`, do not invoke the Fraud Alert Manager MCP and return the skipped result. +4. For every other supplied status, validate all fields required by the alert creation request. +5. Build the request body using the exact field mapping in these instructions. +6. Invoke the Fraud Alert Manager MCP operation that creates an alert exactly once. +7. Wait for the complete MCP response. +8. Preserve the complete original input and append `alert_manager_result` to the returned JSON. + +Never invoke the create-alert operation more than once for the same agent execution, including after a timeout or ambiguous response. Do not call list, get, update, replace, or delete operations as part of this workflow. + +--- + +## MCP Alert Creation + +Use the Create Alert MCP operation. + +The request body must contain exactly these five top-level fields: + +```json +{ + "transaction_id": "string", + "transaction_information": {}, + "overall_compliance": {}, + "decision_support": {}, + "regulatory_interpretation": {} +} +``` + +Do not include `aml_regulatory_assessment`, evidence records, account enrichments, Markdown reports, server-managed fields, or any additional properties in the MCP request. + +### Exact Field Mapping + +Map the request as follows: + +| MCP request field | Input source | +| --- | --- | +| `transaction_id` | `transaction_id`, falling back to `original_transaction.transaction_id` only when the top-level field is absent | +| `transaction_information.originator_name` | `original_transaction.originator_name` | +| `transaction_information.origin_account` | `original_transaction.origin_account` | +| `transaction_information.bank_origin` | `original_transaction.bank_origin` | +| `transaction_information.beneficiary_name` | `original_transaction.beneficiary_name` | +| `transaction_information.destination_account` | `original_transaction.destination_account` | +| `transaction_information.bank_destination` | `original_transaction.bank_destination` | +| `transaction_information.amount` | `original_transaction.amount` | +| `transaction_information.currency` | `original_transaction.currency` | +| `overall_compliance` | `aml_regulatory_assessment.overall_compliance` | +| `decision_support` | `aml_regulatory_assessment.decision_support` | +| `regulatory_interpretation` | `aml_regulatory_assessment.regulatory_interpretation` | + +Copy all mapped values unchanged. Do not summarize, normalize, rename, infer, recalculate, or repair them. + +The complete MCP request has this structure: + +```json +{ + "transaction_id": "TX-TEST-0001", + "transaction_information": { + "originator_name": "James Carter", + "origin_account": "83D4B1F30", + "bank_origin": "0121", + "beneficiary_name": "Emily Foster", + "destination_account": "818CCA030", + "bank_destination": "29196", + "amount": 15000, + "currency": "EUR" + }, + "overall_compliance": { + "overall_status": "POTENTIAL_GAP", + "non_compliant_rule_count": 0, + "potential_gap_rule_count": 1, + "insufficient_data_rule_count": 0, + "not_applicable_rule_count": 0, + "compliant_rule_count": 2 + }, + "decision_support": { + "calculation_version": "1.0", + "calculation_inputs": { + "highest_historical_context_level": 4, + "non_compliant_rule_count": 0, + "potential_gap_rule_count": 1, + "insufficient_data_rule_count": 0, + "not_applicable_rule_count": 0, + "compliant_rule_count": 2 + }, + "investigation_priority": { + "base_historical_context_score": 50, + "potential_gap_points": 10, + "insufficient_data_points": 0, + "non_compliance_points": 0, + "raw_score": 60, + "score": 60, + "maximum_score": 100, + "priority_band": "High" + }, + "workflow_classification": { + "final_verdict": "ENHANCED REVIEW REQUIRED", + "matched_rule_id": "LEVEL_C", + "rationale": [ + "The highest historical context level is 4.", + "One potential regulatory gap remains unresolved.", + "No rule was assessed as non-compliant." + ], + "meaning": "The transaction has significant historical AML relevance and unresolved regulatory validation gaps, but no confirmed regulatory breach was identified.", + "is_legal_conclusion": false, + "establishes_money_laundering": false + } + }, + "regulatory_interpretation": { + "summary": "The transaction has regulatory relevance and an unresolved validation gap.", + "key_findings": [ + "One potential regulatory gap remains unresolved." + ], + "regulatory_relevance": "This assessment validates the transaction against retrieved AML policies." + } +} +``` + +--- + +## Required Request Validation + +Before invoking the MCP, confirm that: + +- `transaction_id` is a string. +- Every `transaction_information` string field is present and is a string. +- `transaction_information.amount` is a number. +- `overall_compliance` contains `overall_status` and all five rule-count fields. +- Every rule count is a non-negative integer. +- `decision_support` contains `calculation_version`, `calculation_inputs`, `investigation_priority`, and `workflow_classification`. +- `regulatory_interpretation` contains `summary`, `key_findings`, and `regulatory_relevance`. +- `key_findings` and `workflow_classification.rationale` are arrays of strings. +- `workflow_classification.is_legal_conclusion` and `workflow_classification.establishes_money_laundering` are booleans. + +If any required field is absent or has the wrong type, do not invoke the MCP. Do not fabricate a default value or derive a replacement. + +--- + +## MCP Response Handling + +A successful create response is expected to contain the submitted alert data and these server-managed fields: + +- `id` +- `created_at` +- `updated_at` +- `version` + +Treat the alert as created only when the MCP operation reports success and returns a non-empty string `id`. + +If the MCP returns a validation error, transport error, timeout, malformed response, or no alert ID, return an MCP error result. Do not retry automatically and do not claim that an alert was created. + +Do not expose credentials, connection details, internal reasoning, or raw stack traces in the result. + +--- + +## Output Requirements + +Return exactly one valid JSON object and no other text. + +- Preserve the complete input JSON unchanged. +- Append exactly one new top-level element named `alert_manager_result`. +- Do not return Markdown or code fences. +- Do not replace or remove any existing field. + +### Alert Created + +When the threshold is met and the MCP creates the alert: + +```json +{ + "transaction_id": "TX-TEST-0001", + "original_transaction": {}, + "aml_regulatory_assessment": {}, + "alert_manager_result": { + "status": "ALERT_CREATED", + "threshold_status": "POTENTIAL_GAP", + "mcp_invoked": true, + "alert_id": "string", + "created_at": "string or null", + "version": 1 + } +} +``` + +Copy `alert_id`, `created_at`, and `version` from the MCP response. Use `null` for `created_at` or `version` only when the alert ID confirms creation but that server-managed field is absent. + +### Alert Skipped + +When `overall_status` is `COMPLIANT`: + +```json +{ + "transaction_id": "TX-TEST-0001", + "original_transaction": {}, + "aml_regulatory_assessment": {}, + "alert_manager_result": { + "status": "ALERT_SKIPPED", + "threshold_status": "COMPLIANT", + "mcp_invoked": false, + "reason": "The overall regulatory status is COMPLIANT." + } +} +``` + +### Invalid Input + +When the threshold cannot be evaluated or the request cannot be built from the supplied input: + +```json +{ + "transaction_id": "TX-TEST-0001", + "original_transaction": {}, + "aml_regulatory_assessment": {}, + "alert_manager_result": { + "status": "ALERT_INPUT_INVALID", + "threshold_status": null, + "mcp_invoked": false, + "error": { + "code": "ALERT_REQUEST_INVALID", + "message": "The input does not contain all fields required to create the alert.", + "missing_or_invalid_fields": [ + "field.path" + ] + } + } +} +``` + +Set `threshold_status` to the supplied status when it is a valid string; otherwise use `null`. + +### MCP Error + +When the create-alert MCP operation fails or its result cannot confirm creation: + +```json +{ + "transaction_id": "TX-TEST-0001", + "original_transaction": {}, + "aml_regulatory_assessment": {}, + "alert_manager_result": { + "status": "ALERT_CREATION_FAILED", + "threshold_status": "NON_COMPLIANT", + "mcp_invoked": true, + "error": { + "code": "ALERT_MCP_ERROR", + "message": "The Fraud Alert Manager did not confirm alert creation." + } + } +} +``` + +--- + +## Forbidden Actions + +- Do not create an alert when `overall_status` is `COMPLIANT`. +- Do not suppress an alert for any other supplied overall status. +- Do not recalculate or override regulatory status, scores, counts, priority bands, or verdicts. +- Do not use individual rule evaluations to replace the overall-status threshold. +- Do not alter names, accounts, bank IDs, amount, currency, findings, rationale, or interpretations. +- Do not invent missing request fields. +- Do not submit fields that are not part of the `FraudAlertCreate` request schema. +- Do not claim that fraud or money laundering occurred. +- Do not recommend blocking, freezing, rejecting, approving, or reporting the transaction. + +--- + +## Final Rules + +- `COMPLIANT` is the only status that does not create an alert. +- Invoke the create-alert MCP operation at most once per execution. +- Use the exact API field mapping defined above. +- Preserve the complete input and append only `alert_manager_result`. +- Return JSON only. diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/azure.yaml b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/azure.yaml new file mode 100644 index 000000000..df201114c --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/azure.yaml @@ -0,0 +1,41 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +requiredVersions: + extensions: + azure.ai.agents: '>=1.0.0-beta.4' +name: fraud-intelligence-orchestration +services: + ai-project: + host: azure.ai.project + fraud-intelligence-orchestration: + host: azure.ai.agent + metadata: + tags: + - Agent Framework + - AI Agent Hosting + - Azure AI AgentServer + - Responses Protocol + - Streaming + project: src + language: python + uses: + - ai-project + kind: hosted + name: fraud-intelligence-orchestration + description: An orchestration of AI agents for fraud intelligence + protocols: + - protocol: responses + version: 2.0.0 + container: + resources: + cpu: '0.5' + memory: 1.0Gi + environmentVariables: + - name: AZURE_AI_AGENTSERVER_DISABLE_EXPERIMENTAL_WARNING + value: true + codeConfiguration: + runtime: python_3_14 + entryPoint: main.py + dependencyResolution: remote_build +infra: + provider: microsoft.foundry diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/run-agent.sh b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/run-agent.sh new file mode 100644 index 000000000..4f12bdc0b --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/run-agent.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +VENV_DIR="${HOME}/venvs/fraud-intelligence" + +python3 -m venv "$VENV_DIR" +source "$VENV_DIR/bin/activate" +trap 'deactivate 2>/dev/null || true' EXIT + +cd "$SCRIPT_DIR/src" + +python -m pip install -r requirements.txt +python main.py + +deactivate +trap - EXIT \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/src/.azdignore b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/src/.azdignore new file mode 100644 index 000000000..0c34457b0 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/src/.azdignore @@ -0,0 +1 @@ +.env.example diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/src/.dockerignore b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/src/.dockerignore new file mode 100644 index 000000000..0d6619bae --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/src/.dockerignore @@ -0,0 +1,7 @@ +.venv +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +.env \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/src/Dockerfile b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/src/Dockerfile new file mode 100644 index 000000000..eaffb94f1 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/src/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY . user_agent/ +WORKDIR /app/user_agent + +RUN if [ -f requirements.txt ]; then \ + pip install -r requirements.txt; \ + else \ + echo "No requirements.txt found"; \ + fi + +EXPOSE 8088 + +CMD ["python", "main.py"] \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/src/main.py b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/src/main.py new file mode 100644 index 000000000..3bf2830a7 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/src/main.py @@ -0,0 +1,92 @@ +# Copyright (c) Microsoft. All rights reserved. + +import os + +from agent_framework import AgentExecutor, WorkflowBuilder +from agent_framework.foundry import FoundryAgent +from agent_framework_foundry_hosting import ResponsesHostServer +from azure.ai.agentserver.core.tasks import set_resilient_tasks_enabled +from azure.identity import DefaultAzureCredential +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +def get_foundry_agent( + *, + project_endpoint: str, + credential: DefaultAzureCredential, + agent_name: str, + agent_version: str, +) -> FoundryAgent: + print(f"Getting Foundry agent: {agent_name} v{agent_version}") + return FoundryAgent( + project_endpoint=project_endpoint, + agent_name=agent_name, + agent_version=agent_version, + credential=credential, + ) + + +def main(): + set_resilient_tasks_enabled(True) + print("=== Fraud intelligence orchestration ===\n") + + project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + credential = DefaultAzureCredential() + + evidence_enrichment_agent = get_foundry_agent( + project_endpoint=project_endpoint, + credential=credential, + agent_name="EvidenceEnrichmentAgent", + agent_version="7", + ) + + regulatory_assessment_agent = get_foundry_agent( + project_endpoint=project_endpoint, + credential=credential, + agent_name="RegulatoryAssessmentAgent", + agent_version="2", + ) + + aml_report_agent = get_foundry_agent( + project_endpoint=project_endpoint, + credential=credential, + agent_name="AMLReportAgent", + agent_version="2", + ) + + alert_manager_agent = get_foundry_agent( + project_endpoint=project_endpoint, + credential=credential, + agent_name="AlertManagerAgent", + agent_version="7", + ) + + # Set the context mode to `last_agent` so that each agent only sees the output of the + # previous agent instead of the full conversation history + evidence_enrichment_executor = AgentExecutor(evidence_enrichment_agent, context_mode="last_agent") + regulatory_assessment_executor = AgentExecutor(regulatory_assessment_agent, context_mode="last_agent") + aml_report_executor = AgentExecutor(aml_report_agent, context_mode="last_agent") + alert_manager_executor = AgentExecutor(alert_manager_agent, context_mode="last_agent") + + workflow_agent = ( + WorkflowBuilder( + start_executor=evidence_enrichment_executor, + # Limiting the output to only the final formatted result. + # If this is not set, all intermediate results will be included in the output. + output_from=[aml_report_executor], + ) + .add_edge(evidence_enrichment_executor, regulatory_assessment_executor) + .add_edge(regulatory_assessment_executor, aml_report_executor) + .add_edge(regulatory_assessment_executor, alert_manager_executor) + .build() + .as_agent() + ) + + server = ResponsesHostServer(workflow_agent) + server.run() + + +if __name__ == "__main__": + main() diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/src/requirements.txt b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/src/requirements.txt new file mode 100644 index 000000000..113cb1571 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/orchestration/src/requirements.txt @@ -0,0 +1,14 @@ +agent-framework-core==1.14.0 +agent-framework-foundry==1.11.0 +agent-framework-foundry-hosting==1.0.0b260813 +agent-framework-orchestrations==1.1.0 +azure-identity +azure-ai-projects>=2.3.0 +azure-cosmos>=4.9.0 +httpx>=0.28,<1 +opentelemetry-exporter-otlp-proto-grpc +python-dotenv +pydantic + +# debugpy enables local debugging of this agent with the Foundry Toolkit VS Code extension. +debugpy \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/solution-05.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/solution-05.md new file mode 100644 index 000000000..566215761 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-05/solution-05.md @@ -0,0 +1,1003 @@ +# Challenge 5 - Govern Models and MCP Servers + +[Previous challenge](solution-04.md) | **[Home](../README.md)** | [Next challenge](solution-06.md) + +## 🎯 Objective + +Introduce the **AI Gateway tier (preview)** for governed model and MCP access, generate an MCP interface from the Fraud Alert Manager API, build an **Alert Manager Agent**, and run alert creation in parallel with report generation. + +## 🧭 Context and Background + +The investigation workflow now produces a decision, but a decision that requires action must also reach the operational alert system. AI Gateway provides a shared policy and observability boundary for model and tool traffic. + +```mermaid +flowchart LR + ASSESS[Regulatory assessment] --> FORK{Parallel completion} + FORK --> REPORT[AML Report Agent] + FORK --> ALERT[Alert Manager Agent] + ALERT --> AMCP[Fraud Alert Manager MCP] + AMCP --> API[Fraud Alert Manager API] + + ORCH[Hosted orchestration] --> GATEWAY[AI Gateway tier] + GATEWAY --> MODEL[Model deployment] + GATEWAY --> FMCP[Financial Evidence MCP] + GATEWAY --> AMCP +``` + +The Alert Manager Agent must treat the regulatory assessment as authoritative. It decides whether to call the alert tool according to an explicit rule, but it does not re-evaluate the transaction. + +## ✅ Tasks + +Before starting, source `baseenv` and `hackenv` to load the environment variables used by this challenge. + +```bash +source baseenv +source hackenv +``` + +### 1. Deploy the AI Gateway + +An **AI Gateway** is a centralized control point for securing and managing interactions among AI agents, models, and external services. It routes requests and responses through a governance layer that can enforce authentication, authorization, rate limiting, and other policies. This helps protect sensitive information, support compliance, and provide observability into AI-driven workflows. + +In Azure, API Management provides the AI Gateway. In this lab, you will deploy a dedicated API Management instance using the preview `AIGateway` SKU, then route model requests and MCP calls through this centralized governance layer. + +#### Create the Gateway + +During the preview, you can create the `AIGateway` SKU through the **AI Gateway portal**, but not through the public Azure CLI or ARM REST API. + +Open the [AI Gateway portal](https://ai.gateway.azure.com/) and sign in with the lab account. Create a new AI Gateway with these settings: + +- **Name**: enter a globally unique name +- **Subscription**: the lab subscription +- **Region**: `swedencentral` +- **Resource group**: the existing lab resource group +- **Enable managed identity** + +![AI Gateway deployment screenshot](images/ai-gateway-deployment.png) + +### 2. Configure Governed Model Access + +#### Import Models + +In the AI Gateway left navigation pane, select **Models**, then select **Add Models**. + +Models can be imported from several sources and providers. For this lab, select models deployed in **Microsoft Foundry**: + +![Import models from Microsoft Foundry](images/import-models-from-microsoft-foundry.png) + +Choose the lab subscription and Foundry resource, then select **Next**: + +![Select subscription and Foundry resource](images/select-subscription-and-foundry-resource.png) + +Keep the default settings, or adjust the names if needed, then select **Create** to import the models into the AI Gateway: + +![Import models into the AI Gateway](images/import-models-into-ai-gateway.png) + +After a successful import, the newly added models appear under **Models**: + +![Models listed under the AI Gateway](images/models-listed-under-ai-gateway.png) + +#### Configure Agents to Access Models Through the AI Gateway + +After importing the models, configure the agents to access them through the AI Gateway. In **Microsoft Foundry**, open **Manage**, select **Resource details**, and then select **Admin-connected models**: + +![Admin-connected models in Microsoft Foundry](images/admin-connected-models-in-microsoft-foundry.png) + +Select **Add** to open the connection dialog. + +Because the **AI Gateway** SKU is still in preview, select **Other source** instead of **Azure API Management**. Enter the following details: + +- **Connection name**: unique identifier, for instance, `aigateway` +- **Base URL** + 1) Get the Gateway models endpoint from the **AI Gateway** portal: + + ![Get the Gateway models endpoint from the AI Gateway portal](images/get-gateway-models-endpoint.png) + + 2) Get the access key. Create new ones: + + ![Create new access key in the AI Gateway portal](images/create-new-access-key.png) + + Copy the access key and keep it for next steps. Click **Next**. + +- **Authentication**: Select **API Key** and enter the access key you obtained in the previous step. As header name, use `api-key`. Then, **Next** + +Finally, select **Add Model** and configure the following values: + +- **Name**: gpt-5.6-luna +- **Display name**: gpt-5.6-luna +- **Version**: leave it empty +- **Format**: OpenAI + +Select **Save** to finish adding the model, then select **Add** to complete the connection. + +The newly added models should now appear under **Admin-connected models** in Microsoft Foundry: + +![Admin-connected models in Microsoft Foundry](images/admin-connected-models-in-microsoft-foundry-after-adding.png) + +Next, add the new admin-connected model to your agents so they can use it through the AI Gateway. + +Open the **Agents** section in Microsoft Foundry, select the agent you want to configure, such as `EvidenceEnrichmentAgent`, and change its **Model** to the newly added admin-connected model: + +![Select the newly added admin-connected model for the agent](images/select-admin-connected-model-for-agent.png) + +Select **Save** to apply the configuration changes. This creates a new agent version. + +Then test the agent. If you configured `EvidenceEnrichmentAgent`, use this JSON payload: + +```json +{ + "transaction_id": "TX-TEST-0001", + "originator_name": "James Carter", + "origin_account": "83D4B1F30", + "bank_origin": "0121", + "beneficiary_name": "Emily Foster", + "destination_account": "818CCA030", + "bank_destination": "29196", + "amount": 15000, + "currency": "EUR" +} +``` + +The agent should use the newly added admin-connected model to return a response based on the transaction details. + +Return to the **AI Gateway** portal to monitor requests and responses for the newly added admin-connected model. Open **Monitoring** and select **Configure telemetry**. + +![Configure telemetry in the AI Gateway portal](images/configure-telemetry-in-ai-gateway-portal.png) + +Use the existing **Application Insights** instance to monitor telemetry. Select the appropriate instance, then select **Next**: + +![Select Application Insights instance for telemetry in the AI Gateway portal](images/select-application-insights-instance-for-telemetry-in-ai-gateway-portal.png) + +Keep **System-assigned managed identity** as the authentication method, review the configuration, and select **Apply**. + +Allow a minute for the configuration to take effect before looking for telemetry. + +Test the agent again, then review the **Monitoring** section in the AI Gateway portal. Telemetry may take a few moments to appear. You should see a metrics view similar to the following: + +![Telemetry graphs in the AI Gateway portal](images/telemetry-graphs-in-ai-gateway-portal.png) + +If time permits, apply the same model changes to all prompt agents. + +#### Apply a Rate-Limiting Policy + +When multiple agents call the same admin-connected model simultaneously, unrestricted usage can degrade the service. Apply a rate-limiting policy to control usage for each caller identity. + +Open **Models**, select `gpt-5.6-luna`, switch to the **Policies** tab, and select **Add policy**: + +![Add rate limiting policy for the admin-connected model](images/add-rate-limiting-policy-for-admin-connected-model.png) + +Select the **Token rate limit** policy: + +![Select Token rate limit policy in the AI Gateway portal](images/select-token-rate-limit-policy-in-ai-gateway-portal.png) + +Set the desired rate-limit parameters. Keep **Caller identity** as the target; in this case, the identity is the key used to connect Microsoft Foundry to the AI Gateway model: + +![Set rate limit parameters in the AI Gateway portal](images/set-rate-limit-parameters-in-ai-gateway-portal.png) + +Select **Create** to apply the rate-limiting policy. + +Requests that exceed the configured limit will now be throttled, helping to maintain fair usage and service availability. + + +### 3. Proxy the Financial Evidence MCP + +The **AI Gateway** can proxy existing Model Context Protocol (MCP) servers. This applies access, authentication, and rate-limiting policies at the gateway without requiring changes to the MCP implementation. + +Next, onboard the existing `financial evidence` MCP to the AI Gateway. In the **AI Gateway** portal, open the **MCP servers** section and select **Add MCP server**: + +![Add MCP server in the AI Gateway portal](images/add-mcp-server-in-ai-gateway-portal.png) + +The portal supports three backend types: + +![Supported backend types in the AI Gateway portal](images/supported-backend-types-in-ai-gateway-portal.png) + +Select **MCP server**, then provide the details required to connect to the existing Financial Evidence MCP: + +- **Backend Name**: A friendly name for the MCP server, for instance, `financial-evidence-mcp`. +- **URL**: The URL of the existing `financial evidence` MCP server. Use the value returned by `echo "$financialEvidenceMcpEndpoint"` +- **Authentication**: set **API Key**, with: + - **Header Name**: `x-functions-key` + - **Value:** Use the value returned by `echo "$financialEvidenceMcpSystemKey"`. + +Select **Next**, validate the configuration, and select **Create** to add the MCP server to the **AI Gateway**. + +You can now test the MCP from the **AI Gateway**. Select **Use**: + +![Test the MCP from the AI Gateway](images/test-mcp-from-ai-gateway.png) + +Select the **Try it** tab, then select **List tools**. You should see all tools exposed by the Financial Evidence MCP: + +![List tools in the financial evidence MCP](images/list-tools-in-financial-evidence-mcp.png) + +Select the **Get Bank Information** operation, enter `0121` as the bank ID, and select **Run tool**: + +![Run Get Bank Information tool in the financial evidence MCP](images/run-get-bank-information-tool-in-financial-evidence-mcp.png) + +The result should display information for bank ID `0121`. Expand the `data` element to view the details: + +![View detailed bank information in the financial evidence MCP](images/view-detailed-bank-information-in-financial-evidence-mcp.png) + +You can now interact with the Financial Evidence MCP through the **AI Gateway**. Next, configure `EvidenceEnrichmentAgent` to use it. + +#### Update the EvidenceEnrichmentAgent Configuration + +Return to the **Microsoft Foundry** portal. Under **Build**, select **Agents**, then select `EvidenceEnrichmentAgent`. Remove the existing tool to ensure that the agent uses the latest MCP configuration: + +![Remove existing tool from the EvidenceEnrichmentAgent](images/remove-existing-tool-from-evidenceenrichmentagent.png) + +Select **Save**, then open the **Tools** menu on the left side of the page. + +Select the `financial-evidence-mcp` tool and update its URL and authentication: + +- **Remote MCP Server endpoint**: Use the Financial Evidence MCP URL exposed through the **AI Gateway**. +- **Credential**: Set the header name to `api-key` and use an existing key from your **AI Gateway** configuration. + +Then select **Update**. + +On the same MCP configuration page, select **Use in an agent**: + +![Use the MCP in an agent](images/use-mcp-in-agent.png) + +Select `EvidenceEnrichmentAgent` as the agent that will use this MCP. + +Under the `EvidenceEnrichmentAgent` **Tools** section, verify that the `financial-evidence-mcp` endpoint shows the new **AI Gateway** configuration. Enable **Always auto-approve all tools**: + +![Configure Always auto-approve all tools](images/configure-always-auto-approve-all-tools.png) + +**Save** the agent again to apply the new configuration. + +Test the agent and inspect its traces to verify that it uses `financial-evidence-mcp` through the **AI Gateway**. + +To explore additional gateway policies, open the **Policies** section for `financial-evidence-mcp`. Because the **AI Gateway** is in preview, some MCP features, including the **Monitoring** tab, may still be unavailable. + + +### 4. Generate an MCP from the Fraud Alert Manager API + +The **AI Gateway** can also expose an existing API as an MCP, bringing agent integration and policy management to APIs that were not originally designed as MCP servers. + +For this scenario, the Fraud Alert Manager API manages financial alerts so that operational users can review them and take action. A simple web interface is also available for validation. + +First, retrieve the API endpoint so that the AI Gateway can read its OpenAPI specification. The API runs on **Azure Container Apps**: + +```bash +endpoint=$(az containerapp show --name alert-manager --resource-group $rg --query properties.configuration.ingress.fqdn -o tsv) +echo "https://$endpoint/openapi.json" +``` +Copy the URL; you will use it to configure the MCP in the **AI Gateway**. + +Return to **MCP servers** in the **AI Gateway** portal. Follow the previous process using a different backend. + +Select **Add MCP server**, choose **OpenAPI Specification**, and complete the form with the following details: + +- **Name**: Provide a name for the MCP server, e.g., `alert-manager-mcp`. +- **Spec source**: Select **Fetch from URL** +- **Spec URL**: Paste the URL you obtained from the previous step. +- **Authentication**: Select **None** because the lab API does not require authentication. + +![Add MCP server](images/add-mcp-server-from-api.png) + +Select **Next**, validate the configuration, and select **Create** to add the MCP server. + +Explore the new MCP server in the **AI Gateway** playground. For example, list the existing alerts: + +![List existing alerts](images/list-existing-alerts.png) + + +Next, create `AlertManagerAgent` to interact with `alert-manager-mcp` through the **AI Gateway**, then add the agent to the full orchestration. + +### 5. Create the AlertManagerAgent + +This is the fourth agent in the workflow. `AlertManagerAgent` uses `alert-manager-mcp` to manage financial alerts through the **AI Gateway**. + +Use the following configuration: + +- Use the agent instructions in `walkthrough/challenge-05/alert-manager-agent/instructions.md`. +- Select the model routed through the **AI Gateway**. +- Onboard the MCP in **Microsoft Foundry** following the usual steps for MCP integration. Remember to add authentication with **API Key**, using header name `api-key` and value a valid API key provided by the **AI Gateway**. +- Configure the MCP for the agent and enable auto-approval for all tools. +- Save the agent configuration. + +The supplied instructions require the agent to accept the regulatory assessment without changing it, create an alert only when the configured status requires one, and surface tool failures without claiming success. The agent sends only the operational data required by the API and uses the transaction ID as a stable case key. + +You can verify alert creation by inspecting the agent traces and confirming that the MCP call succeeded. + +Alternatively, use the Alert Manager web interface to view the current alerts. + +The web interface runs in the same container as the API. Retrieve its URL: + +```bash +endpoint=$(az containerapp show --name alert-manager --resource-group $rg --query properties.configuration.ingress.fqdn -o tsv) +echo "https://$endpoint" +``` + +Open the URL. Before testing, the dashboard contains five alerts: + +![List of current alerts](images/list-of-current-alerts.png) + +Test `AlertManagerAgent` by sending it the following JSON payload: + +```json +{ + "transaction_id": "TX-TEST-0001", + "original_transaction": { + "transaction_id": "TX-TEST-0001", + "originator_name": "James Carter", + "origin_account": "83D4B1F30", + "bank_origin": "0121", + "beneficiary_name": "Emily Foster", + "destination_account": "818CCA030", + "bank_destination": "29196", + "amount": 15000, + "currency": "EUR" + }, + "data_agent_response": { + "transaction_id": "TX-TEST-0001", + "match_found": true, + "assessment": "Historical laundering records were found for one or more accounts in the transaction.", + "origin_account": { + "originator_name": "James Carter", + "account": "83D4B1F30", + "bank_id": "0121", + "bank_name": "Israel Bank #35", + "country": "Israel", + "historical_participation": true, + "role": "Receiver", + "laundering_transaction_count": 3, + "laundering_attempt_count": 3, + "first_seen": "2026-09-02T10:03:00Z", + "last_seen": "2026-09-18T05:02:00Z", + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "evidence": [ + { + "pattern_txn_sk": 120259084422, + "attempt_id": "2502", + "pattern_type": "GATHER-SCATTER", + "step_in_attempt": 5, + "date_key": 20260918, + "txn_ts": "2026-09-18T05:02:00Z", + "from_bank_id": "0015", + "from_account": "84221F9F0", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 44947.3, + "receiving_currency": "Shekel", + "amount_paid": 44947.3, + "payment_currency": "Shekel", + "payment_format": "ACH", + "counterparty_bank_id": "0015", + "counterparty_account": "84221F9F0", + "participation_role": "Receiver" + }, + { + "pattern_txn_sk": 68719477003, + "attempt_id": "1385", + "pattern_type": "FAN-OUT", + "step_in_attempt": 1, + "date_key": 20260909, + "txn_ts": "2026-09-09T02:52:00Z", + "from_bank_id": "0015", + "from_account": "84221D110", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 23427.81, + "receiving_currency": "Shekel", + "amount_paid": 23427.81, + "payment_currency": "Shekel", + "payment_format": "ACH", + "counterparty_bank_id": "0015", + "counterparty_account": "84221D110", + "participation_role": "Receiver" + }, + { + "pattern_txn_sk": 8589934791, + "attempt_id": "150", + "pattern_type": "BIPARTITE", + "step_in_attempt": 13, + "date_key": 20260902, + "txn_ts": "2026-09-02T10:03:00Z", + "from_bank_id": "0220", + "from_account": "8000EB430", + "to_bank_id": "0121", + "to_account": "83D4B1F30", + "amount_received": 58403.73, + "receiving_currency": "Shekel", + "amount_paid": 58403.73, + "payment_currency": "Shekel", + "payment_format": "ACH", + "counterparty_bank_id": "0220", + "counterparty_account": "8000EB430", + "participation_role": "Receiver" + } + ] + }, + "destination_account": { + "beneficiary_name": "Emily Foster", + "account": "818CCA030", + "bank_id": "29196", + "bank_name": "Bank of Topeka", + "country": "Bangladesh", + "historical_participation": false, + "role": null, + "laundering_transaction_count": 0, + "laundering_attempt_count": 0, + "first_seen": null, + "last_seen": null, + "pattern_types": [], + "evidence": [] + } + }, + "historical_context": { + "accounts_with_history": 1, + "accounts_without_history": 1, + "highest_historical_context_level": 5, + "transaction_interpretation": "One account involved in the transaction has historical AML participation." + }, + "origin_account_enrichment": { + "historical_context_level": 5, + "historical_context_category": "Extensive Historical Participation", + "participation_consistency": "Receiver Only", + "participation_frequency": "Repeated", + "pattern_diversity_count": 3, + "pattern_diversity_level": "High", + "days_since_last_seen": 12, + "recency_band": "Recent", + "investigator_interpretation": "The account appeared across several AML typologies and multiple independent laundering attempts." + }, + "destination_account_enrichment": { + "historical_context_level": 0, + "historical_context_category": "No Historical AML Participation", + "participation_consistency": "None", + "participation_frequency": "None", + "pattern_diversity_count": 0, + "pattern_diversity_level": "None", + "days_since_last_seen": null, + "recency_band": "None", + "investigator_interpretation": "No historical AML participation was identified." + }, + "derived_features": { + "origin_has_history": true, + "destination_has_history": false, + "multiple_pattern_presence": true, + "recent_historical_activity": true, + "highest_historical_context_level": 5 + }, + "aml_regulatory_inputs": { + "historical_context_level": 5, + "pattern_diversity_count": 3, + "laundering_attempt_count": 3, + "recent_participation": true, + "historical_participation_role": "Receiver", + "historical_participation_detected": true + }, + "aml_regulatory_assessment": { + "assessment_source": "kb-aml", + "assessment_type": "AML regulation and policy compliance validation", + "regulatory_scope": { + "global_rules_evaluated": true, + "supranational_rules_evaluated": true, + "origin_country_rules_evaluated": true, + "destination_country_rules_evaluated": true, + "cross_border_rules_evaluated": true, + "historical_aml_context_rules_evaluated": true, + "origin_country": "Israel", + "destination_country": "Bangladesh", + "is_cross_border": true + }, + "retrieved_policy_sets": [ + { + "policy_set_id": "GLB-STD-001", + "policy_set_name": "Global AML/CFT Standards Summary", + "scope": "GLOBAL", + "jurisdiction": null, + "summary": "Risk-based AML controls, CDD, enhanced due diligence, wire-transfer transparency, monitoring, reporting, sanctions screening and record keeping.", + "source_reference": "01_Global_AML_CFT_Standards_Summary.pdf【6:9†source_name】" + }, + { + "policy_set_id": "GLB-STD-001", + "policy_set_name": "Supranational FATF-style Framework", + "scope": "SUPRANATIONAL", + "jurisdiction": null, + "summary": "Supranational risk-based controls apply using origin and destination geography and relevant transaction indicators.", + "source_reference": "01_Global_AML_CFT_Standards_Summary.pdf【6:9†source_name】" + }, + { + "policy_set_id": "IL-REG-016", + "policy_set_name": "Israel AML Local Regulation", + "scope": "ORIGIN_COUNTRY", + "jurisdiction": "Israel", + "summary": "Israel identification, beneficiary and controlling-shareholder declaration, cross-border reporting and suspicious-activity obligations.", + "source_reference": "16_Israel_AML_Local_Regulation.pdf【6:16†source_name】" + }, + { + "policy_set_id": "BD-REG-017", + "policy_set_name": "Bangladesh AML Local Regulation", + "scope": "DESTINATION_COUNTRY", + "jurisdiction": "Bangladesh", + "summary": "Bangladesh customer identification, suspicious-activity reporting and outward-remittance documentation requirements.", + "source_reference": "17_Bangladesh_AML_Local_Regulation.pdf【6:3†source_name】【6:1†source_name】" + }, + { + "policy_set_id": "GLB-STD-001", + "policy_set_name": "Cross-Border AML and Wire-Transfer Policy", + "scope": "CROSS_BORDER", + "jurisdiction": "Israel to Bangladesh", + "summary": "Cross-border transfers require accurate originator and beneficiary information, risk-based review and applicable jurisdictional controls.", + "source_reference": "01_Global_AML_CFT_Standards_Summary.pdf【6:10†source】【6:11†source】" + }, + { + "policy_set_id": "HIST-AML-001", + "policy_set_name": "Historical AML Participation Review", + "scope": "HISTORICAL_AML_CONTEXT", + "jurisdiction": null, + "summary": "Historical participation and repeated typology indicators are relevant inputs to transaction monitoring and enhanced review.", + "source_reference": "01_Global_AML_CFT_Standards_Summary.pdf【6:6†source】" + } + ], + "rule_evaluations": [ + { + "rule_id": "AML-GLOBAL-CDD-001", + "rule_name": "Customer due diligence and beneficial-owner verification", + "policy_set_name": "Global AML/CFT Standards Summary", + "scope": "GLOBAL", + "jurisdiction": null, + "rule_summary": "CDD applies to relevant transactions and requires identity, beneficial-owner, purpose and ongoing monitoring information.", + "applies_to_transaction": true, + "compliance_status": "INSUFFICIENT_DATA", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 5, + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "recent_historical_activity": true + }, + "interpretation": "Names and account identifiers are present, but the input does not establish identity verification, beneficial-owner verification, purpose, source of funds or source of wealth.", + "kb_source_reference": "01_Global_AML_CFT_Standards_Summary.pdf【6:9†source】【6:10†source】" + }, + { + "rule_id": "AML-GLOBAL-TRM-001", + "rule_name": "Transaction monitoring for historical AML indicators", + "policy_set_name": "Historical AML Participation Review", + "scope": "HISTORICAL_AML_CONTEXT", + "jurisdiction": null, + "rule_summary": "Monitoring must consider co-occurring typology indicators and prior AML participation.", + "applies_to_transaction": true, + "compliance_status": "COMPLIANT", + "validation_result": { + "passed": true, + "failed": false, + "missing_required_data": false + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 5, + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "recent_historical_activity": true + }, + "interpretation": "The evidence contains repeated participation, three pattern types, three attempts and recent activity, providing the historical indicators required for monitoring.", + "kb_source_reference": "01_Global_AML_CFT_Standards_Summary.pdf【6:6†source】" + }, + { + "rule_id": "AML-CROSS-BORDER-WIRE-001", + "rule_name": "Cross-border wire-transfer information", + "policy_set_name": "Cross-Border AML and Wire-Transfer Policy", + "scope": "CROSS_BORDER", + "jurisdiction": "Israel to Bangladesh", + "rule_summary": "Cross-border messages must contain required and accurate originator and beneficiary information.", + "applies_to_transaction": true, + "compliance_status": "INSUFFICIENT_DATA", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 5, + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "recent_historical_activity": true + }, + "interpretation": "Originator and beneficiary names and account identifiers are supplied, but address, national identification or date/place of birth and transfer-message completeness are not provided.", + "kb_source_reference": "01_Global_AML_CFT_Standards_Summary.pdf【6:10†source】【6:11†source】" + }, + { + "rule_id": "AML-IL-001", + "rule_name": "Israel identification and cross-border reporting", + "policy_set_name": "Israel AML Local Regulation", + "scope": "ORIGIN_COUNTRY", + "jurisdiction": "Israel", + "rule_summary": "Relevant Israeli institutions must perform required identification, record the purpose and corridor information, and report suspicious activity where suspicion arises.", + "applies_to_transaction": true, + "compliance_status": "POTENTIAL_GAP", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 5, + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "recent_historical_activity": true + }, + "interpretation": "Israel is the origin jurisdiction and the transfer is cross-border. The input does not show completed identification, declared beneficiary or controlling shareholder, purpose, or reporting assessment.", + "kb_source_reference": "16_Israel_AML_Local_Regulation.pdf【6:4†source】" + }, + { + "rule_id": "AML-BD-001", + "rule_name": "Bangladesh remittance purpose and supporting documentation", + "policy_set_name": "Bangladesh AML Local Regulation", + "scope": "DESTINATION_COUNTRY", + "jurisdiction": "Bangladesh", + "rule_summary": "Outward-remittance controls require authorised purpose and supporting documentation; transaction and suspicious-activity requirements apply where relevant.", + "applies_to_transaction": true, + "compliance_status": "POTENTIAL_GAP", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 5, + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "recent_historical_activity": true + }, + "interpretation": "Bangladesh is the destination jurisdiction, but the input does not provide the transfer purpose, authorised purpose code, licensed channel evidence or supporting documentation.", + "kb_source_reference": "17_Bangladesh_AML_Local_Regulation.pdf【6:1†source】" + }, + { + "rule_id": "AML-GLOBAL-EDD-001", + "rule_name": "Enhanced review for complex or unusually large activity", + "policy_set_name": "Global AML/CFT Standards Summary", + "scope": "SUPRANATIONAL", + "jurisdiction": null, + "rule_summary": "Complex or unusually large transactions without apparent lawful purpose and higher geographic or historical exposure require enhanced due-diligence consideration.", + "applies_to_transaction": true, + "compliance_status": "POTENTIAL_GAP", + "validation_result": { + "passed": false, + "failed": false, + "missing_required_data": true + }, + "evidence_used": { + "transaction_id": "TX-TEST-0001", + "origin_country": "Israel", + "destination_country": "Bangladesh", + "origin_bank_id": "0121", + "destination_bank_id": "29196", + "amount": 15000, + "currency": "EUR", + "origin_historical_participation": true, + "destination_historical_participation": false, + "highest_historical_context_level": 5, + "pattern_types": [ + "FAN-OUT", + "GATHER-SCATTER", + "BIPARTITE" + ], + "recent_historical_activity": true + }, + "interpretation": "The corridor includes Israel and Bangladesh, and one account has extensive recent historical participation across multiple typologies. Enhanced-review evidence such as senior approval, purpose and source-of-funds information is absent.", + "kb_source_reference": "01_Global_AML_CFT_Standards_Summary.pdf【6:9†source】【6:10†source】" + } + ], + "overall_compliance": { + "overall_status": "POTENTIAL_GAP", + "non_compliant_rule_count": 0, + "potential_gap_rule_count": 3, + "insufficient_data_rule_count": 2, + "not_applicable_rule_count": 0, + "compliant_rule_count": 1 + }, + "decision_support": { + "calculation_version": "1.0", + "calculation_inputs": { + "highest_historical_context_level": 5, + "non_compliant_rule_count": 0, + "potential_gap_rule_count": 3, + "insufficient_data_rule_count": 2, + "not_applicable_rule_count": 0, + "compliant_rule_count": 1 + }, + "investigation_priority": { + "base_historical_context_score": 65, + "potential_gap_points": 30, + "insufficient_data_points": 10, + "non_compliance_points": 0, + "raw_score": 105, + "score": 100, + "maximum_score": 100, + "priority_band": "Critical" + }, + "workflow_classification": { + "final_verdict": "ENHANCED REVIEW REQUIRED", + "matched_rule_id": "LEVEL_C", + "rationale": [ + "The highest historical context level is 5.", + "Three potential regulatory gaps were identified.", + "Two applicable rules cannot be fully assessed because required data is absent.", + "No retrieved rule was assessed as clearly non-compliant." + ], + "meaning": "The transaction has substantial regulatory relevance and unresolved validation gaps. This is a workflow classification, not a legal conclusion.", + "is_legal_conclusion": false, + "establishes_money_laundering": false + } + }, + "regulatory_interpretation": { + "summary": "The transaction is cross-border from Israel to Bangladesh and involves an origin account with extensive, recent participation across three historical AML typologies. The available data supports transaction-monitoring validation, but does not establish completion of CDD, enhanced due diligence, wire-transfer data requirements, purpose validation or local reporting controls.", + "key_findings": [ + "The origin account has historical participation level 5, three laundering attempts and three pattern types.", + "The destination account has no recorded historical AML participation.", + "Israel and Bangladesh country rules are applicable because the transaction crosses both jurisdictions.", + "Originator and beneficiary names and account identifiers are present, but additional wire-transfer identity fields are absent.", + "Purpose, source-of-funds, beneficial-owner, sanctions-screening and reporting-status fields are absent.", + "No direct regulatory breach was established from the retrieved rules and supplied evidence." + ], + "regulatory_relevance": "The transaction requires regulatory validation because of cross-border scope and extensive historical AML context. This assessment does not determine fraud, suspicious activity, money laundering or a mandatory operational outcome." + }, + "missing_data": [ + { + "field": "customer_due_diligence_status", + "required_for_rule": "AML-GLOBAL-CDD-001", + "reason": "The input does not establish identity verification or completion of CDD." + }, + { + "field": "beneficial_owner_information", + "required_for_rule": "AML-GLOBAL-CDD-001 and AML-IL-001", + "reason": "No beneficial-owner or controlling-shareholder information is supplied." + }, + { + "field": "purpose_and_intended_nature", + "required_for_rule": "AML-GLOBAL-CDD-001 and AML-BD-001", + "reason": "The transaction purpose and intended nature are not provided." + }, + { + "field": "originator_address_or_identification_data", + "required_for_rule": "AML-CROSS-BORDER-WIRE-001", + "reason": "The retrieved cross-border wire rule requires address, national ID, or date and place of birth information." + }, + { + "field": "beneficiary_address_or_identification_data", + "required_for_rule": "AML-CROSS-BORDER-WIRE-001", + "reason": "The input does not provide the additional beneficiary information required by the retrieved wire-transfer policy." + }, + { + "field": "sanctions_screening_result", + "required_for_rule": "AML-GLOBAL-EDD-001", + "reason": "No sanctions-screening result is supplied." + }, + { + "field": "source_of_funds_and_source_of_wealth", + "required_for_rule": "AML-GLOBAL-CDD-001 and AML-GLOBAL-EDD-001", + "reason": "No source-of-funds or source-of-wealth evidence is supplied." + }, + { + "field": "bangladesh_authorised_purpose_code_and_supporting_documentation", + "required_for_rule": "AML-BD-001", + "reason": "The Bangladesh policy requires purpose and supporting documentation for applicable remittance activity." + }, + { + "field": "suspicious_activity_reporting_assessment", + "required_for_rule": "AML-IL-001 and global reporting controls", + "reason": "The input does not state whether the institution performed or documented a reporting assessment." + } + ], + "downstream_inputs": { + "has_regulatory_non_compliance": false, + "has_potential_regulatory_gap": true, + "requires_case_recommendation_review": true, + "highest_historical_context_level": 5, + "rules_triggered": [ + "AML-GLOBAL-CDD-001", + "AML-GLOBAL-TRM-001", + "AML-CROSS-BORDER-WIRE-001", + "AML-IL-001", + "AML-BD-001", + "AML-GLOBAL-EDD-001" + ], + "countries_evaluated": [ + "Israel", + "Bangladesh" + ], + "policy_scopes_evaluated": [ + "GLOBAL", + "SUPRANATIONAL", + "ORIGIN_COUNTRY", + "DESTINATION_COUNTRY", + "CROSS_BORDER", + "HISTORICAL_AML_CONTEXT" + ] + } + } +} +``` + +Confirm that the new alert appears on the Alert Management dashboard: + +![Alert Management dashboard showing the new alert](images/alert-management-dashboard.png) + +You can view the alert details or remove the alert to avoid duplicates in later tests. + + +### 6. Add the Alert Manager Agent to the Orchestration + +The final task is to add `AlertManagerAgent` to the orchestration so it can process transactions that require operational alerting. + +As in [Challenge 4](solution-04.md), update the orchestration to include and connect the new agent. + +The source code is under the `walkthrough/challenge-05/orchestration` directory. The relevant files are: + +- `src/main.py`: Initializes and coordinates the agents in the orchestration. +- `azure.yaml`: Defines the agent for deployment as a **Hosted Agent** in **Microsoft Foundry**. + +Review the addition of the Alert Manager Agent: + +```python + alert_manager_agent = get_foundry_agent( + project_endpoint=project_endpoint, + credential=credential, + agent_name="AlertManagerAgent", + agent_version="7", + ) +``` + +The workflow then runs the AML Report Agent and Alert Manager Agent as parallel branches after regulatory assessment: + +```python + workflow_agent = ( + WorkflowBuilder( + start_executor=evidence_enrichment_executor, + # Limiting the output to only the final formatted result. + # If this is not set, all intermediate results will be included in the output. + output_from=[aml_report_executor], + ) + .add_edge(evidence_enrichment_executor, regulatory_assessment_executor) + .add_edge(regulatory_assessment_executor, aml_report_executor) + .add_edge(regulatory_assessment_executor, alert_manager_executor) + .build() + .as_agent() + ) +``` + +> **Important:** Review your agent versions in case you need to update them before deployment. + +Deploy the updated orchestration. + +> **Important:** Because of current source-code handling limitations in the extension, copy the orchestration source and `azure.yaml` to the project root before deployment: + +```bash +cp -Rf \ + "$walkthroughHome/challenge-05/orchestration/src" \ + "$walkthroughHome/challenge-05/orchestration/azure.yaml" \ + "$rootHome/" +``` + +For the complete **Foundry Toolkit** deployment procedure, refer to [Challenge 4](solution-04.md). For this deployment: + +- Keep **Deployment method** set to **Code** and **Package mode** set to **Remote**. +- Set **Deploy to** to **Existing agent** because this deployment updates the orchestration with `AlertManagerAgent`. + +You can now test the full orchestration through the **Foundry Toolkit** or directly in **Microsoft Foundry**. + +Inspect the traces to verify that the orchestration invokes the new agent and that the alert appears in the dashboard. If you retained the alert from the standalone agent test, a repeated invocation may create a duplicate. + +Use these additional JSON payloads to test the orchestration: + +- A transaction with no fraudulent activity in the database: + +```json +{ + "transaction_id": "TX-TEST-0002", + "originator_name": "Oliver Thompson", + "origin_account": "844C89040", + "bank_origin": "3160527", + "beneficiary_name": "Sophie Williams", + "destination_account": "842700DC0", + "bank_destination": "237304", + "amount": 75000, + "currency": "EUR" +} +``` + +- A transaction that remains within the EU and has no evidence: + +```json +{ + "transaction_id": "TX-TEST-0003", + "originator_name": "Daniel Harris", + "origin_account": "80870EE60", + "bank_origin": "220048", + "beneficiary_name": "Charlotte Evans", + "destination_account": "8099ADB50", + "bank_destination": "31585", + "amount": 5000, + "currency": "EUR" +} +``` + +Finally, inspect the AI Gateway telemetry and agent traces to verify that model calls and both MCP integrations traverse the gateway. Confirm that logs do not expose credentials or unnecessary financial payloads. Where possible, send an unauthorized request and verify that the gateway rejects it. + +## 🚀 Go Further + +Add per-agent quotas and compare their effects under a short concurrent workload. Define a retry policy that respects API throttling and does not create duplicate alerts. + +## 🛠️ Troubleshooting + +- **Gateway requests are unauthorized:** Verify the caller identity, gateway audience or credential, role assignments, and backend authentication policy. +- **MCP tools are missing:** Confirm that the API operation is included in the imported specification and supported by the generated MCP interface. +- **An MCP tool name exceeds 64 characters:** Use a shorter MCP server name or shorter OpenAPI `operationId`, recreate the MCP integration, and start a new agent conversation. +- **Evidence calls bypass the gateway:** Check the agent's active version and MCP endpoint configuration. +- **Duplicate alerts are created:** Remove test alerts between runs and ensure that retries use the same stable case key. +- **A parallel branch hides a failure:** Inspect the traces and require each branch to return an explicit status. + +## 🧠 Conclusion + +You have placed model and MCP traffic behind a governance boundary and added operational alerting in parallel with report generation. Continue to [Challenge 6](solution-06.md) to add operational and business observability to the complete workflow. diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/azure.yaml b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/azure.yaml new file mode 100644 index 000000000..df201114c --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/azure.yaml @@ -0,0 +1,41 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +requiredVersions: + extensions: + azure.ai.agents: '>=1.0.0-beta.4' +name: fraud-intelligence-orchestration +services: + ai-project: + host: azure.ai.project + fraud-intelligence-orchestration: + host: azure.ai.agent + metadata: + tags: + - Agent Framework + - AI Agent Hosting + - Azure AI AgentServer + - Responses Protocol + - Streaming + project: src + language: python + uses: + - ai-project + kind: hosted + name: fraud-intelligence-orchestration + description: An orchestration of AI agents for fraud intelligence + protocols: + - protocol: responses + version: 2.0.0 + container: + resources: + cpu: '0.5' + memory: 1.0Gi + environmentVariables: + - name: AZURE_AI_AGENTSERVER_DISABLE_EXPERIMENTAL_WARNING + value: true + codeConfiguration: + runtime: python_3_14 + entryPoint: main.py + dependencyResolution: remote_build +infra: + provider: microsoft.foundry diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/run-agent.sh b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/run-agent.sh new file mode 100644 index 000000000..4f12bdc0b --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/run-agent.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +VENV_DIR="${HOME}/venvs/fraud-intelligence" + +python3 -m venv "$VENV_DIR" +source "$VENV_DIR/bin/activate" +trap 'deactivate 2>/dev/null || true' EXIT + +cd "$SCRIPT_DIR/src" + +python -m pip install -r requirements.txt +python main.py + +deactivate +trap - EXIT \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/run-tests.sh b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/run-tests.sh new file mode 100644 index 000000000..37d49a2af --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/run-tests.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +VENV_DIR="${HOME}/venvs/fraud-intelligence" + +python3 -m venv "$VENV_DIR" +source "$VENV_DIR/bin/activate" +trap 'deactivate 2>/dev/null || true' EXIT + +cd "$SCRIPT_DIR/src" + +python -m pip install -r requirements.txt + +if [[ $# -eq 0 ]]; then + set -- --count 10 +fi + +python hosted_agent_client.py "$@" + +deactivate +trap - EXIT \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/.azdignore b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/.azdignore new file mode 100644 index 000000000..0c34457b0 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/.azdignore @@ -0,0 +1 @@ +.env.example diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/.dockerignore b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/.dockerignore new file mode 100644 index 000000000..0d6619bae --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/.dockerignore @@ -0,0 +1,7 @@ +.venv +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +.env \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/Dockerfile b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/Dockerfile new file mode 100644 index 000000000..eaffb94f1 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY . user_agent/ +WORKDIR /app/user_agent + +RUN if [ -f requirements.txt ]; then \ + pip install -r requirements.txt; \ + else \ + echo "No requirements.txt found"; \ + fi + +EXPOSE 8088 + +CMD ["python", "main.py"] \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/business_metrics.py b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/business_metrics.py new file mode 100644 index 000000000..708ccfcb1 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/business_metrics.py @@ -0,0 +1,189 @@ +import json +import logging +from collections.abc import Callable +from typing import Any + +from agent_framework import AgentExecutorResponse, WorkflowContext, executor +from agent_framework.observability import get_meter + +logger = logging.getLogger(__name__) +meter = get_meter("fraud_intelligence.business") + +transaction_amount = meter.create_histogram( + "fraud.transaction.amount", + unit="{currency}", + description="Transaction amount in its original currency", +) +transactions_processed = meter.create_counter( + "fraud.transactions.processed", + unit="{transaction}", +) +historical_context_level = meter.create_histogram( + "fraud.aml.historical_context_level", + unit="1", +) +evidence_count = meter.create_histogram( + "fraud.aml.evidence_count", + unit="{record}", +) +typology_detected = meter.create_counter( + "fraud.aml.typology_detected", + unit="{detection}", +) +rules_evaluated = meter.create_counter( + "fraud.regulatory.rules_evaluated", + unit="{rule}", +) +missing_data_count = meter.create_histogram( + "fraud.regulatory.missing_data_count", + unit="{field}", +) +priority_score = meter.create_histogram( + "fraud.investigation.priority_score", + unit="1", +) +workflow_classifications = meter.create_counter( + "fraud.workflow.classifications", + unit="{transaction}", +) +review_required = meter.create_counter( + "fraud.workflow.review_required", + unit="{transaction}", +) +alert_status = meter.create_counter( + "fraud.alert.status", + unit="{alert}", +) +metric_extraction_failures = meter.create_counter( + "fraud.metrics.extraction_failures", + unit="{failure}", +) + + +def parse_agent_json(text: str) -> dict[str, Any]: + content = text.strip() + if content.startswith("```"): + lines = content.splitlines()[1:] + if lines and lines[-1].strip() == "```": + lines.pop() + content = "\n".join(lines) + return json.loads(content) + + +def record_evidence_metrics(payload: dict[str, Any]) -> None: + transaction = payload["original_transaction"] + data_response = payload["data_agent_response"] + features = payload["derived_features"] + origin = data_response["origin_account"] + destination = data_response["destination_account"] + + attributes = { + "currency": transaction["currency"], + "cross_border": origin.get("country") != destination.get("country"), + "history_detected": features["origin_has_history"] or features["destination_has_history"], + } + transaction_amount.record(transaction["amount"], attributes) + transactions_processed.add(1, attributes) + + for account_role, account in (("origin", origin), ("destination", destination)): + enrichment = payload[f"{account_role}_account_enrichment"] + historical_context_level.record( + enrichment["historical_context_level"], + {"account_role": account_role}, + ) + evidence_count.record(len(account.get("evidence", [])), {"account_role": account_role}) + + pattern_types = set(origin.get("pattern_types", []) + destination.get("pattern_types", [])) + for pattern_type in pattern_types: + typology_detected.add(1, {"pattern_type": pattern_type}) + + +def record_regulatory_metrics(payload: dict[str, Any]) -> None: + assessment = payload["aml_regulatory_assessment"] + compliance = assessment["overall_compliance"] + decision_support = assessment["decision_support"] + priority = decision_support["investigation_priority"] + classification = decision_support["workflow_classification"] + + for rule in assessment["rule_evaluations"]: + rules_evaluated.add( + 1, + { + "status": rule["compliance_status"], + "scope": rule["scope"], + }, + ) + + missing_data_count.record( + len(assessment.get("missing_data", [])), + {"overall_status": compliance["overall_status"]}, + ) + priority_score.record(priority["score"], {"priority_band": priority["priority_band"]}) + workflow_classifications.add( + 1, + { + "verdict": classification["final_verdict"], + "priority_band": priority["priority_band"], + "overall_status": compliance["overall_status"], + }, + ) + review_required.add( + 1, + {"required": assessment["downstream_inputs"]["requires_case_recommendation_review"]}, + ) + + +def record_alert_metrics(payload: dict[str, Any]) -> None: + result = payload["alert_manager_result"] + alert_status.add( + 1, + { + "status": result["status"], + "threshold_status": result["threshold_status"], + "mcp_invoked": result["mcp_invoked"], + }, + ) + + +async def observe_metrics( + response: AgentExecutorResponse, + ctx: WorkflowContext[AgentExecutorResponse, None], + *, + stage: str, + recorder: Callable[[dict[str, Any]], None], +) -> None: + try: + recorder(parse_agent_json(response.agent_response.text)) + except Exception: + logger.exception("Could not extract business metrics from %s response", stage) + metric_extraction_failures.add(1, {"stage": stage}) + finally: + await ctx.send_message(response) + + +@executor(id="evidence_metrics", input=AgentExecutorResponse, output=AgentExecutorResponse) +async def evidence_metrics_executor( + response: AgentExecutorResponse, + ctx: WorkflowContext[AgentExecutorResponse, None], +) -> None: + await observe_metrics(response, ctx, stage="evidence_enrichment", recorder=record_evidence_metrics) + + +@executor(id="regulatory_metrics", input=AgentExecutorResponse, output=AgentExecutorResponse) +async def regulatory_metrics_executor( + response: AgentExecutorResponse, + ctx: WorkflowContext[AgentExecutorResponse, None], +) -> None: + await observe_metrics(response, ctx, stage="regulatory_assessment", recorder=record_regulatory_metrics) + + +@executor(id="alert_metrics", input=AgentExecutorResponse) +async def alert_metrics_executor( + response: AgentExecutorResponse, + ctx: WorkflowContext[None, None], +) -> None: + try: + record_alert_metrics(parse_agent_json(response.agent_response.text)) + except Exception: + logger.exception("Could not extract business metrics from alert_manager response") + metric_extraction_failures.add(1, {"stage": "alert_manager"}) diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/hosted_agent_client.py b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/hosted_agent_client.py new file mode 100644 index 000000000..22d60fd95 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/hosted_agent_client.py @@ -0,0 +1,327 @@ +import argparse +import json +import os +import random +import string +import uuid +from dataclasses import dataclass +from typing import Any + +from azure.ai.projects import AIProjectClient +from azure.cosmos import CosmosClient +from azure.identity import DefaultAzureCredential +from dotenv import load_dotenv + +DEFAULT_AGENT_NAME = "fraud-intelligence-orchestration" +DEFAULT_DATABASE_NAME = "aml" +DEFAULT_BANK_CONTAINER = "dim-bank" +DEFAULT_PATTERN_CONTAINER = "fact-laundering-pattern-txn" +MAX_BANK_SELECTION_ATTEMPTS = 20 + +CURRENCIES = ("EUR", "USD", "GBP", "CHF", "JPY", "CAD", "AUD") +ORIGINATOR_NAMES = ( + "James Carter", + "Sophia Bennett", + "Liam Anderson", + "Olivia Hughes", + "Daniel Morgan", + "Ava Richardson", + "Lucas Thompson", + "Mia Sullivan", + "Alexander Wright", + "Charlotte Evans", + "Mateo Garcia", + "Sofia Martinez", + "Hugo Laurent", + "Camille Bernard", + "Luca Romano", + "Giulia Conti", + "Jonas Schneider", + "Anna Fischer", + "Kenji Sato", + "Yuki Tanaka", +) +BENEFICIARY_NAMES = ( + "Emily Foster", + "Noah Mitchell", + "Isabella Reed", + "Ethan Brooks", + "Grace Campbell", + "Henry Parker", + "Amelia Ward", + "Benjamin Cooper", + "Victoria Bailey", + "Samuel Turner", + "Diego Lopez", + "Valentina Torres", + "Louis Moreau", + "Manon Dubois", + "Marco Bianchi", + "Elena Rossi", + "Felix Wagner", + "Laura Becker", + "Haruto Suzuki", + "Aoi Nakamura", +) + + +@dataclass(frozen=True) +class Bank: + bank_id: str + bank_name: str + country: str + + +class CosmosTransactionSampler: + def __init__( + self, + client: CosmosClient, + *, + database_name: str, + bank_container_name: str, + pattern_container_name: str, + random_source: random.Random, + ) -> None: + database = client.get_database_client(database_name) + self._banks = database.get_container_client(bank_container_name) + self._patterns = database.get_container_client(pattern_container_name) + self._random = random_source + self._bank_count = self._count(self._banks) + self._pattern_count = self._count(self._patterns) + + if self._bank_count < 2: + raise RuntimeError(f"Container {bank_container_name!r} must contain at least two banks") + if self._pattern_count < 1: + raise RuntimeError(f"Container {pattern_container_name!r} contains no transactions") + + @staticmethod + def _count(container: Any) -> int: + return int( + next( + iter( + container.query_items( + query="SELECT VALUE COUNT(1) FROM c", + enable_cross_partition_query=True, + ) + ) + ) + ) + + def random_bank(self, *, exclude_bank_id: str | None = None) -> Bank: + for _ in range(10): + offset = self._random.randrange(self._bank_count) + items = list( + self._banks.query_items( + query=( + "SELECT c.bank_id, c.bank_name, c.country FROM c " + "ORDER BY c.bank_id OFFSET @offset LIMIT 1" + ), + parameters=[{"name": "@offset", "value": offset}], + enable_cross_partition_query=True, + ) + ) + if items and str(items[0]["bank_id"]) != exclude_bank_id: + return self._to_bank(items[0]) + raise RuntimeError("Could not select a distinct random bank") + + def pattern_account_for_bank(self, bank: Bank) -> tuple[str, str, str] | None: + normalized_bank_id = bank.bank_id.lstrip("0") or "0" + predicates = [ + "c.from_bank_id = @bank_id", + "c.to_bank_id = @bank_id", + ] + parameters: list[dict[str, Any]] = [ + {"name": "@bank_id", "value": bank.bank_id}, + ] + if normalized_bank_id.isdigit(): + predicates.extend( + ( + "StringToNumber(c.from_bank_id) = @numeric_bank_id", + "StringToNumber(c.to_bank_id) = @numeric_bank_id", + ) + ) + parameters.append( + {"name": "@numeric_bank_id", "value": int(normalized_bank_id)} + ) + + items = list( + self._patterns.query_items( + query=( + "SELECT TOP 1 c.from_bank_id, c.from_account, " + "c.to_bank_id, c.to_account FROM c WHERE " + + " OR ".join(predicates) + ), + parameters=parameters, + enable_cross_partition_query=True, + ) + ) + if not items: + return None + + pattern = items[0] + from_bank_id = str(pattern["from_bank_id"]) + if (from_bank_id.lstrip("0") or "0") == normalized_bank_id: + return "origin", from_bank_id, str(pattern["from_account"]) + + to_bank_id = str(pattern["to_bank_id"]) + return "destination", to_bank_id, str(pattern["to_account"]) + + @staticmethod + def _to_bank(item: dict[str, Any]) -> Bank: + return Bank( + bank_id=str(item["bank_id"]), + bank_name=str(item["bank_name"]), + country=str(item["country"]), + ) + + +def random_account(random_source: random.Random) -> str: + return "".join(random_source.choices(string.hexdigits[:16].upper(), k=9)) + + +def build_transaction( + sampler: CosmosTransactionSampler, + *, + use_pattern_account: bool, + random_source: random.Random, +) -> dict[str, Any]: + origin_bank: Bank + destination_bank: Bank + + if use_pattern_account: + for _ in range(MAX_BANK_SELECTION_ATTEMPTS): + selected_bank = sampler.random_bank() + pattern_account = sampler.pattern_account_for_bank(selected_bank) + if pattern_account is None: + continue + + side, pattern_bank_id, account = pattern_account + matched_bank = Bank( + bank_id=pattern_bank_id, + bank_name=selected_bank.bank_name, + country=selected_bank.country, + ) + if side == "origin": + origin_bank = matched_bank + origin_account = account + destination_bank = sampler.random_bank(exclude_bank_id=selected_bank.bank_id) + destination_account = random_account(random_source) + else: + destination_bank = matched_bank + destination_account = account + origin_bank = sampler.random_bank(exclude_bank_id=selected_bank.bank_id) + origin_account = random_account(random_source) + break + else: + raise RuntimeError( + f"Could not find a bank with a laundering-pattern transaction " + f"after {MAX_BANK_SELECTION_ATTEMPTS} attempts" + ) + else: + origin_bank = sampler.random_bank() + destination_bank = sampler.random_bank(exclude_bank_id=origin_bank.bank_id) + origin_account = random_account(random_source) + destination_account = random_account(random_source) + + return { + "transaction_id": f"TX-TEST-{uuid.uuid4().hex[:12].upper()}", + "originator_name": random_source.choice(ORIGINATOR_NAMES), + "origin_account": origin_account, + "bank_origin": origin_bank.bank_id, + "beneficiary_name": random_source.choice(BENEFICIARY_NAMES), + "destination_account": destination_account, + "bank_destination": destination_bank.bank_id, + "amount": random_source.randint(10_000, 100_000), + "currency": random_source.choice(CURRENCIES), + } + + +def invoke_hosted_agent( + project_client: AIProjectClient, + *, + agent_name: str, + payload: dict[str, Any], +) -> str: + openai_client = project_client.get_openai_client(agent_name=agent_name) + response = openai_client.responses.create( + input=json.dumps(payload), + ) + return response.output_text + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Generate transactions from Cosmos DB and invoke the Fraud Intelligence hosted agent." + ) + parser.add_argument("--count", type=int, default=1, help="Number of independent requests to send") + parser.add_argument("--seed", type=int, help="Seed for reproducible data selection") + parser.add_argument("--dry-run", action="store_true", help="Print payloads without invoking the agent") + parser.add_argument("--project-endpoint", default=os.getenv("FOUNDRY_PROJECT_ENDPOINT")) + parser.add_argument("--agent-name", default=os.getenv("FOUNDRY_HOSTED_AGENT_NAME", DEFAULT_AGENT_NAME)) + parser.add_argument("--cosmos-endpoint", default=os.getenv("AZURE_COSMOS_ENDPOINT")) + parser.add_argument("--database", default=os.getenv("AZURE_COSMOS_DATABASE", DEFAULT_DATABASE_NAME)) + parser.add_argument("--bank-container", default=os.getenv("AZURE_COSMOS_BANK_CONTAINER", DEFAULT_BANK_CONTAINER)) + parser.add_argument( + "--pattern-container", + default=os.getenv("AZURE_COSMOS_PATTERN_CONTAINER", DEFAULT_PATTERN_CONTAINER), + ) + args = parser.parse_args() + if args.count < 1: + parser.error("--count must be at least 1") + if not args.cosmos_endpoint: + parser.error("set AZURE_COSMOS_ENDPOINT or pass --cosmos-endpoint") + if not args.project_endpoint and not args.dry_run: + parser.error("set FOUNDRY_PROJECT_ENDPOINT or pass --project-endpoint") + return args + + +def main() -> None: + load_dotenv() + args = parse_args() + random_source = random.Random(args.seed) + + with DefaultAzureCredential() as credential: + cosmos_client = CosmosClient(args.cosmos_endpoint, credential=credential) + sampler = CosmosTransactionSampler( + cosmos_client, + database_name=args.database, + bank_container_name=args.bank_container, + pattern_container_name=args.pattern_container, + random_source=random_source, + ) + + if args.dry_run: + project_client = None + else: + project_client = AIProjectClient( + endpoint=args.project_endpoint, + credential=credential, + allow_preview=True, + ) + + try: + for request_index in range(args.count): + use_pattern_account = request_index % 2 == 1 + payload = build_transaction( + sampler, + use_pattern_account=use_pattern_account, + random_source=random_source, + ) + print(f"\nRequest {request_index + 1}/{args.count} (pattern account: {use_pattern_account})") + print(json.dumps(payload, indent=2)) + + if project_client is not None: + result = invoke_hosted_agent( + project_client, + agent_name=args.agent_name, + payload=payload, + ) + print(f"Response:\n{result}") + finally: + if project_client is not None: + project_client.close() + cosmos_client.close() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/main.py b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/main.py new file mode 100644 index 000000000..40fe88211 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/main.py @@ -0,0 +1,96 @@ +# Copyright (c) Microsoft. All rights reserved. + +import os + +from agent_framework import AgentExecutor, WorkflowBuilder +from agent_framework.foundry import FoundryAgent +from agent_framework_foundry_hosting import ResponsesHostServer +from azure.ai.agentserver.core.tasks import set_resilient_tasks_enabled +from azure.identity import DefaultAzureCredential +from business_metrics import alert_metrics_executor, evidence_metrics_executor, regulatory_metrics_executor +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +def get_foundry_agent( + *, + project_endpoint: str, + credential: DefaultAzureCredential, + agent_name: str, + agent_version: str, +) -> FoundryAgent: + print(f"Getting Foundry agent: {agent_name} v{agent_version}") + return FoundryAgent( + project_endpoint=project_endpoint, + agent_name=agent_name, + agent_version=agent_version, + credential=credential, + ) + + +def main(): + set_resilient_tasks_enabled(True) + print("=== Fraud intelligence orchestration ===\n") + + project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + credential = DefaultAzureCredential() + + evidence_enrichment_agent = get_foundry_agent( + project_endpoint=project_endpoint, + credential=credential, + agent_name="evidenceenrichmentagent", + agent_version="6", + ) + + regulatory_assessment_agent = get_foundry_agent( + project_endpoint=project_endpoint, + credential=credential, + agent_name="RegulatoryAssessmentAgent", + agent_version="3", + ) + + aml_report_agent = get_foundry_agent( + project_endpoint=project_endpoint, + credential=credential, + agent_name="AMLReportAgent", + agent_version="2", + ) + + alert_manager_agent = get_foundry_agent( + project_endpoint=project_endpoint, + credential=credential, + agent_name="AlertManagerAgent", + agent_version="2", + ) + + # Set the context mode to `last_agent` so that each agent only sees the output of the + # previous agent instead of the full conversation history + evidence_enrichment_executor = AgentExecutor(evidence_enrichment_agent, context_mode="last_agent") + regulatory_assessment_executor = AgentExecutor(regulatory_assessment_agent, context_mode="last_agent") + aml_report_executor = AgentExecutor(aml_report_agent, context_mode="last_agent") + alert_manager_executor = AgentExecutor(alert_manager_agent, context_mode="last_agent") + + workflow_agent = ( + WorkflowBuilder( + start_executor=evidence_enrichment_executor, + # Limiting the output to only the final formatted result. + # If this is not set, all intermediate results will be included in the output. + output_from=[aml_report_executor], + ) + .add_edge(evidence_enrichment_executor, evidence_metrics_executor) + .add_edge(evidence_metrics_executor, regulatory_assessment_executor) + .add_edge(regulatory_assessment_executor, regulatory_metrics_executor) + .add_edge(regulatory_metrics_executor, aml_report_executor) + .add_edge(regulatory_metrics_executor, alert_manager_executor) + .add_edge(alert_manager_executor, alert_metrics_executor) + .build() + .as_agent() + ) + + server = ResponsesHostServer(workflow_agent) + server.run() + + +if __name__ == "__main__": + main() diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/requirements.txt b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/requirements.txt new file mode 100644 index 000000000..113cb1571 --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/orchestration/src/requirements.txt @@ -0,0 +1,14 @@ +agent-framework-core==1.14.0 +agent-framework-foundry==1.11.0 +agent-framework-foundry-hosting==1.0.0b260813 +agent-framework-orchestrations==1.1.0 +azure-identity +azure-ai-projects>=2.3.0 +azure-cosmos>=4.9.0 +httpx>=0.28,<1 +opentelemetry-exporter-otlp-proto-grpc +python-dotenv +pydantic + +# debugpy enables local debugging of this agent with the Foundry Toolkit VS Code extension. +debugpy \ No newline at end of file diff --git a/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/solution-06.md b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/solution-06.md new file mode 100644 index 000000000..6d9e7135a --- /dev/null +++ b/03-Azure/01-04-AI/07_Fraud_Intelligence/walkthrough/challenge-06/solution-06.md @@ -0,0 +1,240 @@ +# Challenge 6 - Observe Fraud Intelligence + +[Previous challenge](solution-05.md) | **[Home](../README.md)** | [Finish](finish.md) + +## 🎯 Objective + +Add end-to-end **OpenTelemetry Protocol (OTLP)** tracing and business metrics to the hosted fraud intelligence workflow, send telemetry to **Application Insights**, and visualize operational and business outcomes in **Azure Managed Grafana**. + +## 🧭 Context and Background + +So far, you have built a multi-agent workflow that enriches transactions, assesses regulatory requirements, generates reports, and creates alerts. However, you still lack deep visibility into how the workflow behaves from end to end. In this challenge, you will add that observability. + +Production observability must answer two kinds of questions: whether the system is healthy and whether it is delivering the intended business outcome. A shared correlation identifier connects one transaction to its workflow, agent, model, retrieval, MCP, report, and alert activity. + +```mermaid +flowchart LR + TX[Transaction] --> ORCH[Fraud Intelligence workflow] + ORCH --> AGENTS[Agents and tools] + ORCH --> OTLP[OTLP traces and metrics] + AGENTS --> OTLP + OTLP --> APP[Application Insights] + APP --> GRAFANA[Azure Managed Grafana] + GRAFANA --> TECH[Technical dashboard] + GRAFANA --> BIZ[Business dashboard] +``` + +Telemetry must not become a second copy of sensitive financial data. Capture identifiers, timings, statuses, counts, and bounded classifications; avoid names, account numbers, full prompts, evidence payloads, and secrets. + +## ✅ Tasks + +### 1. Explore default telemetry in Application Insights + +In Challenge 2, you connected **App Insights** to **Microsoft Foundry**, but you did not explore the **App Insights** dashboard to view the default telemetry. + +Open the **Monitor** tab for any agent to review its captured metrics. To access the underlying traces, logs, and custom telemetry, select **Open in Azure Monitor**: + +![Open in Azure Monitor](/challenges/images/open-in-azure-monitor.png) + +You should see a dashboard similar to the one below, showing the default telemetry captured by Application Insights: + +![Application Insights Dashboard](/challenges/images/agents-appins-dashboard.png) + +Explore the sections and panels to understand the default telemetry and how it reflects agent behavior. + +Next, inspect a complete orchestration trace to see how the components interact and how telemetry is correlated across the workflow. + +Select **View Traces with Agent Runs**, then choose a **Dependency**: + +![View Traces with Agent Runs](/challenges/images/view-traces-with-agent-runs.png) + +The detailed trace shows the selected dependency within its orchestration run, including its interactions with other components and the telemetry correlation across the workflow. + +To explore and filter all traces, open **Search** in the **Investigate** section of the left sidebar: + +![Search in Investigate section](/challenges/images/search-in-investigate-section.png) + +Select a trace to review its execution details, including spans, attributes, and related telemetry: + +![Trace Details](/challenges/images/trace-details.png) + +These insights are provided by Application Insights through the agents' default auto-instrumentation. + +Next, add custom telemetry to the orchestration to capture business-specific metrics and additional trace context in Application Insights. + +### 2. Review the custom telemetry implementation + +The updated orchestration adds executors after specific agent executions. These executors inspect the responses and record custom metrics. + +The new code is under `/walkthrough/challenge-06/orchestration`. +Review the main orchestration code to see where the custom telemetry executors run after specific agent executions. The following image highlights the main changes: + +![Main Orchestration Code with Custom Telemetry Executors](/challenges/images/main-orchestration-code-with-custom-telemetry-executors.png) + +The metric definitions and configuration are in `walkthrough/challenge-06/orchestration/src/business_metrics.py`. + +Review this code to understand how each custom metric is defined and recorded. + +### 3. Redeploy the orchestration and generate telemetry + +After reviewing the custom telemetry setup, redeploy the orchestration to generate telemetry. + +> **Important:** Review your agent versions in case you need to update them before redeployment. + + +As in previous deployments, copy `src` and `azure.yaml` to the project root before redeploying: + +```bash +cp -Rf \ + "$walkthroughHome/challenge-06/orchestration/src" \ + "$walkthroughHome/challenge-06/orchestration/azure.yaml" \ + "$rootHome/" +``` + +Then redeploy using the **Foundry Toolkit**. + +After deployment, run the Python test script to send randomized requests and generate telemetry in **Application Insights**. + +To run the script, use the following command: + +```bash +$walkthroughHome/challenge-06/orchestration/run-tests.sh \ + --cosmos-endpoint "$cosmosEndpoint" \ + --database "aml" \ + --project-endpoint "https://$foundryAccountName.services.ai.azure.com/api/projects/$foundryProjectName" \ + --agent-name "fraud-intelligence-orchestration" \ + --count 10 +``` + +The script sends the number of requests specified by the `--count` argument. + +### 4. Verify custom telemetry in Application Insights + +In **Application Insights**, verify that the custom telemetry is being captured. Open **Logs** under **Monitoring**, select the `customMetrics` table, and run a query to view the recorded metrics. + +![Custom Telemetry in Application Insights](/challenges/images/custom-telemetry-in-application-insights.png) + +You should see metrics similar to those shown below: + +![Custom Metrics in Application Insights](/challenges/images/custom-metrics-in-application-insights.png) + +You can also query specific custom metrics directly from **Logs**. Open the selector on the right, choose **KQL mode**, enter a query in the editor, and select **Run**: + +![Custom Metrics Query in Application Insights](/challenges/images/custom-metrics-query-in-application-insights.png) + + +The following example queries provide several views of the custom metrics: + +- Transactions Processed by Currency (Last 24 Hours) + +```kusto +customMetrics +| where timestamp > ago(24h) +| where name == "fraud.transactions.processed" +| extend Currency = tostring(customDimensions["currency"]) +| summarize Transactions = sum(valueSum) + by Currency, bin(timestamp, 15m) +| render timechart +``` +- Transactions Processed by Cross-Border and History Detected (Last 24 Hours) + +```kusto +customMetrics +| where timestamp > ago(24h) +| where name == "fraud.transactions.processed" +| extend + CrossBorder = tostring(customDimensions["cross_border"]), + HistoryDetected = tostring(customDimensions["history_detected"]) +| summarize Transactions = sum(valueSum) + by CrossBorder, HistoryDetected +| render columnchart +``` +- AML Historical Context Level by Account Role (Last 24 Hours) + +```kusto +customMetrics +| where timestamp > ago(24h) +| where name == "fraud.aml.historical_context_level" +| extend AccountRole = tostring(customDimensions["account_role"]) +| summarize + AverageLevel = sum(valueSum) / sum(valueCount), + MaximumLevel = max(valueMax) + by AccountRole, bin(timestamp, 1h) +| render timechart +``` + +- AML Typology Detected by Pattern Type (Last 7 Days) + +```kusto +customMetrics +| where timestamp > ago(7d) +| where name == "fraud.aml.typology_detected" +| extend PatternType = tostring(customDimensions["pattern_type"]) +| summarize Detections = sum(valueSum) by PatternType +| order by Detections desc +| render piechart +``` + +The following image shows the result of the last query: + +![Result of AML Typology Detected by Pattern Type Query](/challenges/images/result-of-aml-typology-detected-by-pattern-type-query.png) + +Explore additional queries and visualizations as needed. + +Next, create **Graphical Dashboards** to visualize these custom metrics interactively. + +### 5. Import the Grafana dashboard + +Use Grafana to create interactive dashboards for the custom metrics queried in Application Insights. + +Return to the **Agents (Preview)** section introduced at the beginning of the lab, then select **Explore in Grafana** to open the Grafana integration: + +![Explore in Grafana](/challenges/images/explore-in-grafana.png) + +Grafana provides prebuilt dashboards and panels for visualizing the default telemetry: + +![Pre-built Dashboards in Grafana](/challenges/images/pre-built-dashboards-in-grafana.png) + +Explore these dashboards to become familiar with the interface. + +Next, create a custom dashboard for the metrics used in this workflow. The provided JSON file defines Grafana panels for the custom metrics. + +Select **Dashboards**, then **New**, and choose **Import**. Upload `walkthrough/challenge-06/grafana/fraud-intelligence-grafana-dashboard.json` to import the dashboard into Grafana. + +Then, use the following settings: + +- **Title**: FraudIntelligence +- **Subscription**: The one associated with your lab +- **Resource Group**: The one associated with your lab +- **Region**: swedencentral +- **Azure Monitor Datasource**: Azure Monitor + +Select the correct **Application Insights** resource, whose name starts with `appi-fraud`. The imported dashboard should display panels similar to those below: + +![Custom metrics Grafana Dashboards](/challenges/images/custom-metrics-grafana-dashboards.png) + +You have now imported the dashboard and visualized the custom metrics in Grafana. + +### 6. Run the final validation + +Run additional requests and confirm that the custom metrics appear in both Application Insights and Grafana. Verify that transaction counts match the number of requests sent and that the dashboard distinguishes currencies, cross-border transactions, historical-context levels, account roles, and detected typologies. + +Inspect a complete orchestration trace and confirm that agent and dependency spans remain correlated. Ensure that no credentials, names, account numbers, full prompts, or evidence payloads appear in the custom telemetry. + +## 🚀 Go Further + +Create an Azure Monitor alert for sustained workflow failures or alert-delivery failures. Define a service-level objective for successful investigations and visualize its error budget. + +## 🛠️ Troubleshooting + +- **No telemetry appears:** Verify the exporter configuration, Application Insights connection, identity permissions, network access, and ingestion delay. +- **Spans are disconnected:** Confirm that trace context and the correlation identifier are propagated across async and parallel branches. +- **Metrics have unexpected counts:** Check retry behavior, metric emission points, and idempotency around completed state transitions. +- **Grafana cannot query Azure Monitor:** Verify the data source, managed identity, subscription access, and Grafana role assignments. +- **Queries are slow or expensive:** Reduce the time range, remove high-cardinality dimensions, and aggregate before visualization. +- **Sensitive data appears:** Stop emitting the attribute, deploy the correction, and follow the lab's process for handling previously ingested data. + +## 🧠 Conclusion + +You have instrumented the complete fraud intelligence workflow and created technical and business views of its behavior. Continue to the [finish page](finish.md) to complete the MicroHack. +