Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions lambda-durablefunction-typescript/FraudDetection-Agent/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# USE UV'S ARM64 PYTHON BASE IMAGE
FROM --platform=linux/arm64 ghcr.io/astral-sh/uv:python3.11-bookworm-slim

# SET WORKING DIRECTORY
WORKDIR /app

# COPY UV FILES
COPY pyproject.toml uv.lock ./

# INSTALL DEPENDENCIES (INCLUDING STANDS AGENTS)
RUN uv sync --frozen --no-cache

# COPY AGENT FILE
COPY agent.py ./

# EXPOSE PORT 8080 (REQUIRED BY AGENTCORE)
EXPOSE 8080

# RUN THE AGENT APP
CMD ["uv", "run", "uvicorn", "agent:app", "--host", "0.0.0.0", "--port", "8080"]
93 changes: 93 additions & 0 deletions lambda-durablefunction-typescript/FraudDetection-Agent/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Dict, Any
from datetime import datetime
from strands import Agent
import random

app = FastAPI(title="Fraud Detection Agent", version="1.0.0")

# Initialize Strands agent
strands_agent = Agent()

class InvocationRequest(BaseModel):
input: Dict[str, Any]

class InvocationResponse(BaseModel):
output: Dict[str, Any]


"""
Fraud risk scoring agent

Returns a risk score between 1-5 based on transaction amount:
- For amounts <= 10000: Returns 1-5 with weighted distribution (60% chance of 2)
- For amounts > 10000: Returns 3-5 only with weighted distribution

Weighting:
- Score 2 appears ~60% of the time (when applicable)
- Score 3 appears ~25% of the time
- Other scores split the remaining probability
"""

@app.post("/invocations", response_model=InvocationResponse)
async def invoke_agent(request: InvocationRequest):
try:
# RETRIENVE THE FULL JSON INPUT OBJECT AS A DICT
input_data = request.input
amount = input_data.get("amount", "")
if not amount:
raise HTTPException(status_code=400, detail="Amount not provided, please provide the 'amount' in USD")

#IF WE WANTED TO SEND TO THE AGENT, WE WOULD INVOKE THE STRANDS_AGENT. FOR SIMPLCITY, WE'RE JUST USING RULE-BASED SCORING
#result = strands_agent(amount)

# Determine risk score based on amount
if amount < 1000:
# Low amount: auto-approve (score 1-2)
risk_score = random.choices(
population=[1, 2],
weights=[60, 40],
k=1
)[0]
elif amount >= 10000:
# Very high amount: always send to fraud (score 5)
risk_score = 5
elif amount == 6500:
# Specific test case: medium risk requiring human verification
risk_score = 3
elif amount >= 5000:
# High amount: return 3-5
# Weight: 3 (~50%), 4 (~30%), 5 (~20%)
risk_score = random.choices(
population=[3, 4, 5],
weights=[50, 30, 20],
k=1
)[0]
else:
# Normal amount (1000-4999): return 1-4
# Weight: 2 (~50%), 3 (~30%), 1 (~15%), 4 (~5%)
risk_score = random.choices(
population=[1, 2, 3, 4],
weights=[15, 50, 30, 5],
k=1
)[0]

# Return response
response = {
'risk_score': risk_score,
'amount': amount
}

return InvocationResponse(output=response)

except Exception as e:
raise HTTPException(status_code=500, detail=f"Agent processing failed: {str(e)}")

@app.get("/ping")
async def ping():
return {"status": "Fraud agent is running and healthy. To use, invoke with {amount:x.xx}"}

if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8080)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[project]
name = "FraudDetectionAgent"
version = "1.0.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.121.2",
"httpx>=0.28.1",
"pydantic>=2.12.4",
"strands-agents>=1.16.0",
"uvicorn[standard]>=0.38.0",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"amount": 5000}
1,289 changes: 1,289 additions & 0 deletions lambda-durablefunction-typescript/FraudDetection-Agent/uv.lock

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "fraud-detection",
"version": "1.0.0",
"main": "src/index.ts",
"scripts": {
"build": "npm install && tsc",
"clean": "rm -rf dist && rm -f FraudDetection.zip && rm -rf node_modules",
"prebuild": "npm run clean",
"test": "jest",
"zip": "zip -r FraudDetection.zip node_modules/ && zip -j FraudDetection.zip dist/*",
"deploy": "aws durable-lambda create-function --function-name fn_FraudDetection --runtime nodejs22.x --role $ROLE_ARN --handler index.handler --code S3Bucket=jlosaws-lambda-packages,S3Key=FraudDetection.zip --memory-size 128 --durable-config '{'ExecutionTimeout':600,'RetentionPeriodInDays':7}' "
},
"keywords": [],
"author": "AWS",
"license": "ISC",
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "^24.3.1",
"jest": "30.1.3",
"ts-jest": "^29.2.5",
"typescript": "^5.9.2"
},
"dependencies": {
"@aws/durable-execution-sdk-js": "^1.0.0",
"@aws-sdk/client-bedrock-agentcore": "^3.935.0"
},
"description": ""
}
202 changes: 202 additions & 0 deletions lambda-durablefunction-typescript/FraudDetection-Lambda/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { withDurableExecution, DurableContext } from "@aws/durable-execution-sdk-js";
import { BedrockAgentCoreClient, InvokeAgentRuntimeCommand } from "@aws-sdk/client-bedrock-agentcore";

const agentRuntimeArn = process.env.AGENT_RUNTIME_ARN;
const agentRegion = process.env.AGENT_REGION || 'us-east-1';

interface transaction {
id: number;
amount: number;
location: string;
vendor: string;
score?: number;
}

interface TransactionResult {
statusCode: number;
body: {
transaction_id: number;
amount: number;
fraud_score?: number;
result?: string;
customerVerificationResult?: string;
};

}

class fraudTransaction implements transaction {

constructor(
public id: number,
public amount: number,
public location: string,
public vendor: string,
public score: number = 0
){}

async authorize(tx: fraudTransaction, cusRejection: boolean = false): Promise<TransactionResult> {
//IMPLEMENT LOGIC TO AUTHORIZE TRANSCATION
console.log(`Authorizing transactionId: ${tx.id}`)

let result: TransactionResult = {
statusCode: 200,
body: {
transaction_id: tx.id,
amount: tx.amount,
fraud_score: tx.score,
result: 'authorized'
}
};

//IF AUTHORIZATION CAME FROM CUSTOMER, INDICATE IN RESPONSE
if (cusRejection){result.body.customerVerificationResult = 'TransactionApproved'};

return result;
}

async suspend(tx: fraudTransaction): Promise<boolean> {
//IMPLEMENT LOGIC TO SUSPEND TRANSCATION
console.log(`Suspending transactionId: ${tx.id}`)
return true;
}

async sendToFraud(tx: fraudTransaction, cusRejection: boolean = false): Promise<TransactionResult> {
//IMPLEMENT LOGIC TO SEND TO FRAUD DEPARTMENT
console.log(`Escalating to fraud department - transactionId: ${tx.id}`)

let result: TransactionResult = {
statusCode: 200,
body: {
transaction_id: tx.id,
amount: tx.amount,
fraud_score: tx.score,
result: 'SentToFraudDept'
}
};

//IF DECLINE CAME FROM CUSTOMER, INDICATE IN RESPONSE
if (cusRejection){result.body.customerVerificationResult = 'TransactionDeclined'};

return result;
}

async sendCustomerNotification(callbackId: string, type: string, tx: fraudTransaction): Promise<void> {
//IMPLEMENT LOGIC TO SEND CUSTOMER NOTIFICATION
if(type === 'email'){
console.log(`Email Notification with callbackId: ${callbackId}`);
} else{
console.log(`SMS Notification with callbackId: ${callbackId}`);
};
}
}



export const handler = withDurableExecution(async (event: transaction, context: DurableContext) => {

// Extract transaction information
const tx = new fraudTransaction(event.id, event.amount, event.location, event.vendor, event?.score ?? 0)


// Step 1: Check Transaction Fraud Score by invoking the Agent
tx.score = await context.step("fraudCheck", async () => {

if (tx.score === 0){
console.log("No score submitted, sending to Fraud Agent for assessment");
if (!agentRuntimeArn) {
throw new Error('AGENT_RUNTIME_ARN environment variable is not set');
}

const client = new BedrockAgentCoreClient({ region: agentRegion });

// Prepare the payload for AgentCore
const payloadJson = JSON.stringify({ input: { amount: tx.amount } });

// Invoke the agent - payload should be Buffer or Uint8Array
const command = new InvokeAgentRuntimeCommand({
agentRuntimeArn: agentRuntimeArn,
qualifier: 'DEFAULT',
payload: Buffer.from(payloadJson, 'utf-8'),
contentType: 'application/json',
accept: 'application/json'
});

const response = await client.send(command);

// Parse the streaming response from AgentCore
// Use the SDK's helper method to transform the stream to string
if (response.response) {
const responseText = await response.response.transformToString();
const result = JSON.parse(responseText);
console.log(result);
// Extract risk_score from the agent's output
if (result.output && result.output.risk_score !== undefined) {
return result.output.risk_score;
}
}

// IF NO VALID RESPONSE FROM AGENT, SEND TO FRAUD
console.log("No valid response from agent, sending to Fraud department for manual review.")
return 5;
}else{
return tx.score;
}
});

console.log("Transaction Score = " + tx.score.toString());

//Low Risk, Authorize Transaction
if (tx.score < 3) return context.step("Authorize", async() => await tx.authorize(tx));
if (tx.score >= 5) return context.step("sendToFraud", async() => await tx.sendToFraud(tx)); //High Risk, Send to Fraud Department

//Potential Fraud Detected
if (tx.score > 2 && tx.score < 5){

//Step 2: Suspend the transaction
const tx_suspended = await context.step("suspendTransaction", async () => await tx.suspend(tx));

//Step 3: Ask cardholder to authorize transaction
const verified = await context.parallel("human-verification", [
// Push verification with callback
(ctx: DurableContext) => ctx.waitForCallback("SendVerificationEmail", async (callbackId: string) => {
await tx.sendCustomerNotification(callbackId, 'email', tx);
}, { timeout: { days: 1 } }),
// SMS verification with callback
(ctx: DurableContext) => ctx.waitForCallback("SendVerificationSMS", async (callbackId: string) => {
await tx.sendCustomerNotification(callbackId, 'sms', tx);
}, { timeout: { days: 1 } })
],
{
maxConcurrency: 2,
completionConfig: {
minSuccessful: 1, // Continue after cardholder verifies (email or sms)
toleratedFailureCount: 0 // Fail immediately if any callback fails
}
}
);

//Step 4: Authorize Transaction or Send to Fraud Department
const result = await context.step("advanceTransaction", async () => {
// Check if verification succeeded (at least one callback succeeded)
// Use hasFailure to check if any verification failed
if(!verified.hasFailure && verified.successCount > 0){
return await tx.authorize(tx, true);
}else{
// Verification failed or was rejected - send to fraud department
return await tx.sendToFraud(tx, true);
}
})

return result;
}

return {
statusCode: 400,
body: {
transaction_id: tx.id,
amount: tx.amount,
fraud_score: tx.score,
result: 'Unknown'
}
};;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "es2021",
"module": "nodenext",
"lib": ["es2021"],
"types": ["node"],
"strict": true,
"moduleResolution": "nodenext",
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Loading