diff --git a/samples-v2/openai_agents/azure.yaml b/samples-v2/openai_agents/azure.yaml new file mode 100644 index 00000000..a79d95bc --- /dev/null +++ b/samples-v2/openai_agents/azure.yaml @@ -0,0 +1,8 @@ +name: durable-functions-openai-agents +metadata: + template: durable-functions-openai-agents@0.0.1 +services: + api: + project: . + language: python + host: function diff --git a/samples-v2/openai_agents/function_app.py b/samples-v2/openai_agents/function_app.py index 1f52fde4..0168ff0b 100644 --- a/samples-v2/openai_agents/function_app.py +++ b/samples-v2/openai_agents/function_app.py @@ -10,7 +10,7 @@ from openai import AsyncAzureOpenAI from agents import set_default_openai_client - +from order_returns.order_return_orchestrators import register_order_return_orchestrators #region Regular Azure OpenAI setup @@ -36,6 +36,7 @@ def get_azure_token(): app = df.DFApp(http_auth_level=func.AuthLevel.FUNCTION) +register_order_return_orchestrators(app) @app.route(route="orchestrators/{functionName}") @app.durable_client_input(client_name="client") @@ -46,6 +47,24 @@ async def orchestration_starter(req: func.HttpRequest, client): response = client.create_check_status_response(req, instance_id) return response +@app.route(route="order_return_processor") +@app.durable_client_input(client_name="client") +async def orchestration_order_return_processor(req: func.HttpRequest, client): + # Extract input data from request body + input_data = None + try: + if req.get_body(): + input_data = req.get_json() + except Exception as e: + return func.HttpResponse( + f"Invalid JSON in request body: {str(e)}", + status_code=400 + ) + + # Starting a new orchestration instance with input data + instance_id = await client.start_new("order_return_processor", client_input=input_data) + response = client.create_check_status_response(req, instance_id) + return response @app.orchestration_trigger(context_name="context") @app.durable_openai_agent_orchestrator diff --git a/samples-v2/openai_agents/infra/abbreviations.json b/samples-v2/openai_agents/infra/abbreviations.json new file mode 100644 index 00000000..6cb66621 --- /dev/null +++ b/samples-v2/openai_agents/infra/abbreviations.json @@ -0,0 +1,12 @@ +{ + "cognitiveServicesAccounts": "oai-", + "dts": "dts-", + "insightsComponents": "appi-", + "managedIdentityUserAssignedIdentities": "id-", + "operationalInsightsWorkspaces": "log-", + "resourcesResourceGroups": "rg-", + "storageStorageAccounts": "st", + "taskhub": "th", + "webServerFarms": "plan-", + "webSitesFunctions": "func-" +} diff --git a/samples-v2/openai_agents/infra/app/dts.bicep b/samples-v2/openai_agents/infra/app/dts.bicep new file mode 100644 index 00000000..5fd069fa --- /dev/null +++ b/samples-v2/openai_agents/infra/app/dts.bicep @@ -0,0 +1,38 @@ +@description('Name of the Durable Task Scheduler') +param name string + +@description('Name of the task hub') +param taskhubname string + +@description('Location for the resource') +param location string + +@description('Tags for the resource') +param tags object = {} + +@description('IP allowlist for the scheduler') +param ipAllowlist array = ['0.0.0.0/0'] + +@description('SKU name') +param skuName string = 'Consumption' + +resource scheduler 'Microsoft.DurableTask/schedulers@2024-10-01-preview' = { + name: name + location: location + tags: tags + properties: { + ipAllowlist: ipAllowlist + sku: { + name: skuName + } + } +} + +resource taskhub 'Microsoft.DurableTask/schedulers/taskHubs@2024-10-01-preview' = { + parent: scheduler + name: taskhubname +} + +output dts_ID string = scheduler.id +output dts_URL string = scheduler.properties.endpoint +output TASKHUB_NAME string = taskhub.name diff --git a/samples-v2/openai_agents/infra/main.bicep b/samples-v2/openai_agents/infra/main.bicep new file mode 100644 index 00000000..318b4b52 --- /dev/null +++ b/samples-v2/openai_agents/infra/main.bicep @@ -0,0 +1,309 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(64) +@description('Name of the environment which is used to generate a short unique hash used in all resources.') +param environmentName string + +@minLength(1) +@description('Primary location for all resources') +@metadata({ + azd: { + type: 'location' + } +}) +param location string + +@description('Name of the resource group') +param resourceGroupName string = '' + +@description('Name of the storage account') +param storageAccountName string = '' + +@description('Name of the app service plan') +param appServicePlanName string = '' + +@description('Name of the API service') +param apiServiceName string = '' + +@description('Name of the user assigned identity') +param apiUserAssignedIdentityName string = '' + +@description('Name of the application insights resource') +param applicationInsightsName string = '' + +@description('Name of the log analytics workspace') +param logAnalyticsName string = '' + +@description('Name of the Durable Task Scheduler') +param dtsName string = '' + +@description('Name of the task hub') +param taskHubName string = '' + +@description('Name of the Azure OpenAI account') +param openAiName string = '' + +@description('Model name for deployment') +param modelName string = 'gpt-4o' + +@description('Model version for deployment') +param modelVersion string = '2024-08-06' + +@description('Model deployment capacity') +param modelCapacity int = 30 + +@description('Id of the user or app to assign application roles') +param principalId string = deployer().objectId + +// Variables +var abbrs = loadJsonContent('./abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } +var functionAppName = !empty(apiServiceName) ? apiServiceName : '${abbrs.webSitesFunctions}${resourceToken}' +var deploymentStorageContainerName = 'app-package-${take(functionAppName, 32)}-${take(toLower(uniqueString(functionAppName, resourceToken)), 7)}' + +// Resource Group +resource rg 'Microsoft.Resources/resourceGroups@2024-03-01' = { + name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' + location: location + tags: tags +} + +// User Assigned Managed Identity +module apiUserAssignedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.0' = { + name: 'apiUserAssignedIdentity-${resourceToken}' + scope: rg + params: { + name: !empty(apiUserAssignedIdentityName) ? apiUserAssignedIdentityName : '${abbrs.managedIdentityUserAssignedIdentities}${resourceToken}' + location: location + tags: tags + } +} + +// Storage Account +module storage 'br/public:avm/res/storage/storage-account:0.29.0' = { + scope: rg + name: 'storage-${resourceToken}' + params: { + name: !empty(storageAccountName) ? storageAccountName : '${abbrs.storageStorageAccounts}${resourceToken}' + location: location + tags: tags + kind: 'StorageV2' + skuName: 'Standard_LRS' + allowSharedKeyAccess: true + networkAcls: { + defaultAction: 'Allow' + } + blobServices: { + containers: [ + { name: deploymentStorageContainerName } + ] + } + roleAssignments: [ + { + principalId: apiUserAssignedIdentity.outputs.principalId + roleDefinitionIdOrName: 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' // Storage Blob Data Owner + principalType: 'ServicePrincipal' + } + { + principalId: apiUserAssignedIdentity.outputs.principalId + roleDefinitionIdOrName: '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3' // Storage Table Data Contributor + principalType: 'ServicePrincipal' + } + { + principalId: apiUserAssignedIdentity.outputs.principalId + roleDefinitionIdOrName: '974c5e8b-45b9-4653-ba55-5f855dd0fb88' // Storage Queue Data Contributor + principalType: 'ServicePrincipal' + } + { + principalId: principalId + roleDefinitionIdOrName: 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' // Storage Blob Data Owner + principalType: 'User' + } + ] + } +} + +// App Service Plan - Flex Consumption +module appServicePlan 'br/public:avm/res/web/serverfarm:0.5.0' = { + name: 'appserviceplan-${resourceToken}' + scope: rg + params: { + name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}' + location: location + tags: tags + skuName: 'FC1' + reserved: true + } +} + +// Log Analytics Workspace +module logAnalytics 'br/public:avm/res/operational-insights/workspace:0.13.0' = { + scope: rg + name: 'logs-${resourceToken}' + params: { + name: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' + location: location + tags: tags + } +} + +// Application Insights +module monitoring 'br/public:avm/res/insights/component:0.7.1' = { + scope: rg + name: 'monitoring-${resourceToken}' + params: { + name: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + location: location + tags: tags + workspaceResourceId: logAnalytics.outputs.resourceId + roleAssignments: [ + { + principalId: apiUserAssignedIdentity.outputs.principalId + roleDefinitionIdOrName: '3913510d-42f4-4e42-8a64-420c390055eb' // Monitoring Metrics Publisher + principalType: 'ServicePrincipal' + } + ] + } +} + +// Azure OpenAI +module openAi 'br/public:avm/res/cognitive-services/account:0.9.2' = { + scope: rg + name: 'openai-${resourceToken}' + params: { + name: !empty(openAiName) ? openAiName : '${abbrs.cognitiveServicesAccounts}${resourceToken}' + location: location + tags: tags + kind: 'OpenAI' + customSubDomainName: !empty(openAiName) ? openAiName : '${abbrs.cognitiveServicesAccounts}${resourceToken}' + disableLocalAuth: true + publicNetworkAccess: 'Enabled' + sku: 'S0' + deployments: [ + { + name: modelName + model: { + format: 'OpenAI' + name: modelName + version: modelVersion + } + sku: { + name: 'GlobalStandard' + capacity: modelCapacity + } + } + ] + roleAssignments: [ + { + principalId: apiUserAssignedIdentity.outputs.principalId + roleDefinitionIdOrName: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' // Cognitive Services OpenAI User + principalType: 'ServicePrincipal' + } + { + principalId: principalId + roleDefinitionIdOrName: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' // Cognitive Services OpenAI User + principalType: 'User' + } + ] + } +} + +// Durable Task Scheduler +module dts './app/dts.bicep' = { + scope: rg + name: 'dts-${resourceToken}' + params: { + name: !empty(dtsName) ? dtsName : '${abbrs.dts}${resourceToken}' + taskhubname: !empty(taskHubName) ? taskHubName : '${abbrs.taskhub}${resourceToken}' + location: location + tags: tags + ipAllowlist: ['0.0.0.0/0'] + skuName: 'Consumption' + } +} + +// DTS Role Assignments +module dtsRoleApi 'br/public:avm/ptn/authorization/resource-role-assignment:0.1.2' = { + scope: rg + name: 'dtsRoleApi-${resourceToken}' + params: { + principalId: apiUserAssignedIdentity.outputs.principalId + roleDefinitionId: '0ad04412-c4d5-4796-b79c-f76d14c8d402' // Durable Task Data Contributor + principalType: 'ServicePrincipal' + resourceId: dts.outputs.dts_ID + } +} + +module dtsRoleUser 'br/public:avm/ptn/authorization/resource-role-assignment:0.1.2' = { + scope: rg + name: 'dtsRoleUser-${resourceToken}' + params: { + principalId: principalId + roleDefinitionId: '0ad04412-c4d5-4796-b79c-f76d14c8d402' // Durable Task Data Contributor + principalType: 'User' + resourceId: dts.outputs.dts_ID + } +} + +// Function App +module api 'br/public:avm/res/web/site:0.19.3' = { + name: 'api-${resourceToken}' + scope: rg + params: { + name: functionAppName + location: location + tags: union(tags, { 'azd-service-name': 'api' }) + kind: 'functionapp,linux' + serverFarmResourceId: appServicePlan.outputs.resourceId + managedIdentities: { + userAssignedResourceIds: [apiUserAssignedIdentity.outputs.resourceId] + } + functionAppConfig: { + deployment: { + storage: { + type: 'blobContainer' + value: 'https://${storage.outputs.name}.blob.${environment().suffixes.storage}/${deploymentStorageContainerName}' + authentication: { + type: 'UserAssignedIdentity' + userAssignedIdentityResourceId: apiUserAssignedIdentity.outputs.resourceId + } + } + } + scaleAndConcurrency: { + instanceMemoryMB: 2048 + maximumInstanceCount: 100 + } + runtime: { + name: 'python' + version: '3.11' + } + } + siteConfig: { + appSettings: [ + { name: 'AzureWebJobsStorage__credential', value: 'managedidentity' } + { name: 'AzureWebJobsStorage__clientId', value: apiUserAssignedIdentity.outputs.clientId } + { name: 'AzureWebJobsStorage__blobServiceUri', value: 'https://${storage.outputs.name}.blob.${environment().suffixes.storage}' } + { name: 'AzureWebJobsStorage__queueServiceUri', value: 'https://${storage.outputs.name}.queue.${environment().suffixes.storage}' } + { name: 'AzureWebJobsStorage__tableServiceUri', value: 'https://${storage.outputs.name}.table.${environment().suffixes.storage}' } + { name: 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING', value: 'Endpoint=${dts.outputs.dts_URL};Authentication=ManagedIdentity;ClientID=${apiUserAssignedIdentity.outputs.clientId}' } + { name: 'TASKHUB_NAME', value: dts.outputs.TASKHUB_NAME } + { name: 'AZURE_OPENAI_ENDPOINT', value: openAi.outputs.endpoint } + { name: 'AZURE_OPENAI_DEPLOYMENT', value: modelName } + { name: 'AZURE_OPENAI_API_VERSION', value: '2025-03-01-preview' } + { name: 'OPENAI_DEFAULT_MODEL', value: modelName } + { name: 'AZURE_CLIENT_ID', value: apiUserAssignedIdentity.outputs.clientId } + { name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: monitoring.outputs.connectionString } + ] + } + } +} + +// Outputs +output AZURE_LOCATION string = location +output AZURE_RESOURCE_GROUP string = rg.name +output AZURE_FUNCTION_APP_NAME string = api.outputs.name +output SERVICE_API_URI string = 'https://${api.outputs.defaultHostname}' +output AZURE_OPENAI_ENDPOINT string = openAi.outputs.endpoint +output AZURE_OPENAI_DEPLOYMENT string = modelName diff --git a/samples-v2/openai_agents/infra/main.parameters.json b/samples-v2/openai_agents/infra/main.parameters.json new file mode 100644 index 00000000..bc915c43 --- /dev/null +++ b/samples-v2/openai_agents/infra/main.parameters.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + } + } +} diff --git a/samples-v2/openai_agents/order_returns/README.md b/samples-v2/openai_agents/order_returns/README.md new file mode 100644 index 00000000..133ee66f --- /dev/null +++ b/samples-v2/openai_agents/order_returns/README.md @@ -0,0 +1,163 @@ +# Order Return Processing Sample + +This sample demonstrates an order return processing system built with Azure Durable Functions and OpenAI Agents. The system automatically processes customer return requests using AI agents to validate return reasons, process refunds, and route edge cases to human review. + +## Running the Sample + +**For complete setup instructions, configuration details, and troubleshooting, see the [Getting Started Guide](/docs/openai_agents/getting-started.md).** + +### Step 1: Start the Azure Functions App + +From the OpenAI Agents samples root directory (`/samples-v2/openai_agents`), start the Azure Functions host: + +```bash +func start +``` + +The function app will start and listen on `http://localhost:7071` by default. + +### Step 2: Submit a Return Request + +```bash +curl -X POST http://localhost:7071/api/order_return_processor \ + -H "Content-Type: application/json" \ + -d '{ + "order_id": "ORD-12345", + "customer_id": "CUST-67890", + "product_category": "Electronics", + "purchase_date": "2024-10-01", + "return_reason": "Product arrived damaged in shipping" + }' +``` + +### Step 3: Check Processing Status + +```bash +# Use the statusQueryGetUri from the submission response +curl http://localhost:7071/runtime/webhooks/durabletask/instances/{instance_id} +``` + +## Testing + +### Test Valid Return (Auto-Approve) + +```bash +curl -X POST http://localhost:7071/api/order_return_processor \ + -H "Content-Type: application/json" \ + -d '{"order_id":"TEST-001","customer_id":"CUST-001","product_category":"Electronics","purchase_date":"2024-10-01","return_reason":"arrived damaged"}' +``` + +### Test Invalid Return (Human Review) + +```bash +curl -X POST http://localhost:7071/api/order_return_processor \ + -H "Content-Type: application/json" \ + -d '{"order_id":"TEST-002","customer_id":"CUST-002","product_category":"Clothing","purchase_date":"2024-09-15","return_reason":"changed my mind"}' +``` + +### Check Status + +Use the `statusQueryGetUri` from the response to check the orchestration status: + +```bash +curl {statusQueryGetUri_from_response} +``` + +## API Reference + +### Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/api/order_return_processor` | Submit a new return request | +| `GET` | `/runtime/webhooks/durabletask/instances/{instance_id}` | Check processing status | + +### Request Schema + +```json +{ + "order_id": "string (required)", + "customer_id": "string (required)", + "product_category": "string (required)", + "purchase_date": "string (required)", + "return_reason": "string (required)" +} +``` + +## Business Logic + +### Validation Rules + +**✅ Auto-Approved Returns** +- Defective or broken products +- Wrong item shipped +- Damaged during shipping +- Product not as described +- Quality issues +- Size/fit problems + +**❌ Requires Human Review** +- Changed mind without valid cause +- Found item cheaper elsewhere +- Buyer's remorse +- Financial reasons +- Vague complaints + +## Example Responses + +### Successful Auto-Approval + +```json +{ + "status": "approved_and_processed", + "validation": { + "agent_response": "Return approved - product defect identified", + "validation_result": { "is_valid": true } + }, + "refund": { + "refund_details": { + "success": true, + "refund_amount": 93.00, + "transaction_id": "REF-ABC123", + "processing_time": "3-5 business days" + } + }, + "message": "Return approved and refund processed successfully" +} +``` + +### Human Review Required + +```json +{ + "status": "pending_human_review", + "validation": { + "agent_response": "Return requires manual review - unclear reason", + "validation_result": { "is_valid": false } + }, + "human_review": { + "review_ticket_id": "REV-XYZ789", + "estimated_review_time": "24-48 hours", + "message": "Your return request has been escalated for human review" + } +} +``` + +## Architecture + +The system uses a multi-agent orchestration pattern: + +1. **Order Return Processor** (Main Orchestrator) - Coordinates the entire workflow +2. **Validation Agent** - Analyzes return reasons against business rules +3. **Refund Agent** - Processes approved refunds automatically +4. **Human Review Activity** - Creates support tickets for manual cases + +## File Structure + +``` +order_returns/ +├── order_return_orchestrators.py # Main orchestration logic +├── order_return_validation.py # Validation agent +├── refund_processing.py # Refund processing agent +└── README.md # This file +``` \ No newline at end of file diff --git a/samples-v2/openai_agents/order_returns/order_return_orchestrators.py b/samples-v2/openai_agents/order_returns/order_return_orchestrators.py new file mode 100644 index 00000000..d964e2e4 --- /dev/null +++ b/samples-v2/openai_agents/order_returns/order_return_orchestrators.py @@ -0,0 +1,111 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import uuid +import azure.durable_functions as df + + +def register_order_return_orchestrators(app: df.DFApp): + """Register all order return related orchestrators and activities with the app""" + + @app.orchestration_trigger(context_name="context") + def order_return_processor(context: df.DurableOrchestrationContext): + """ + Orchestrates the order return process: + 1. Validates return reason using an AI agent + 2. If valid, processes refund via another orchestration + 3. If invalid, routes for human review + """ + + # Get the input data (order return request) + return_request_input = context.get_input() + + if not return_request_input: + return { + "status": "error", + "message": "No return request data provided" + } + + # Step 1: Validate the return reason using AI agent + validation_task = context.call_sub_orchestrator("order_return_validation", return_request_input) + validation_result = yield validation_task + + # Extract validation decision from agent response + is_valid = False + if validation_result: + if isinstance(validation_result, dict) and validation_result.get("validation_result"): + is_valid = validation_result["validation_result"].get("is_valid", False) + elif isinstance(validation_result, str): + # Handle case where validation_result is a string + is_valid = "valid" in validation_result.lower() and "invalid" not in validation_result.lower() + + if is_valid: + # Step 2a: Return is valid - process refund + refund_task = context.call_sub_orchestrator("refund_processor", { + "order_id": return_request_input.get("order_id"), + "customer_id": return_request_input.get("customer_id"), + "validation_result": validation_result + }) + refund_result = yield refund_task + + return { + "status": "approved_and_processed", + "validation": validation_result, + "refund": refund_result, + "message": "Return approved and refund processed successfully" + } + else: + # Step 2b: Return is invalid - route for human review + human_review_task = context.call_activity("route_for_human_review", { + "return_request": return_request_input, + "validation_result": validation_result, + "reason": "Return reason does not meet policy criteria" + }) + review_result = yield human_review_task + + return { + "status": "pending_human_review", + "validation": validation_result, + "human_review": review_result, + "message": "Return requires human review" + } + + @app.orchestration_trigger(context_name="context") + @app.durable_openai_agent_orchestrator + def order_return_validation(context): + """Sub-orchestration that validates return reasons using AI agent""" + import order_returns.order_return_validation + return_request = context.get_input() + return order_returns.order_return_validation.main(return_request) + + @app.orchestration_trigger(context_name="context") + @app.durable_openai_agent_orchestrator + def refund_processor(context): + """Sub-orchestration that processes refunds using AI agent""" + import order_returns.refund_processing + validation_data = context.get_input() + return order_returns.refund_processing.main(validation_data) + + @app.activity_trigger(input_name="review_data") + async def route_for_human_review(review_data: dict) -> dict: + """Activity function that routes invalid returns for human review""" + + # In a real implementation, this would: + # - Create a ticket in a support system + # - Send notification to review team + # - Update order status in database + # - Send email to customer about review process + + review_ticket_id = f"REV-{uuid.uuid4().hex[:8].upper()}" + + # Simulate creating review ticket + print(f"[HUMAN REVIEW] Created review ticket {review_ticket_id}") + print(f"[HUMAN REVIEW] Order ID: {review_data['return_request'].get('order_id')}") + print(f"[HUMAN REVIEW] Reason: {review_data.get('reason')}") + + return { + "review_ticket_id": review_ticket_id, + "status": "pending_review", + "estimated_review_time": "24-48 hours", + "contact_method": "email", + "message": f"Your return request has been escalated for human review. Ticket ID: {review_ticket_id}" + } \ No newline at end of file diff --git a/samples-v2/openai_agents/order_returns/order_return_validation.py b/samples-v2/openai_agents/order_returns/order_return_validation.py new file mode 100644 index 00000000..73bcf9a9 --- /dev/null +++ b/samples-v2/openai_agents/order_returns/order_return_validation.py @@ -0,0 +1,138 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from __future__ import annotations + +from pydantic import BaseModel +from typing import Literal + +from agents import Agent, Runner, function_tool + + +class ReturnValidationResult(BaseModel): + is_valid: bool + reason: str + confidence_score: float # 0.0 to 1.0 + order_id: str + + +class OrderReturnRequest(BaseModel): + order_id: str + return_reason: str + purchase_date: str + product_category: str + customer_id: str + + +@function_tool +def validate_return_reason(return_request: OrderReturnRequest) -> ReturnValidationResult: + """ + Validate if an order return reason is legitimate based on company policy. + Valid reasons include: defective product, wrong item received, damaged in shipping, + not as described, quality issues, size/fit issues (for clothing). + Invalid reasons include: changed mind after 30 days, buyer's remorse, + found cheaper elsewhere, impulse purchase regret. + """ + + # Simulate policy validation logic + valid_reasons = [ + "defective", "damaged", "wrong item", "not as described", + "quality issues", "size issue", "fit issue", "broken", + "missing parts", "expired" + ] + + invalid_reasons = [ + "changed mind", "buyer's remorse", "found cheaper", + "impulse purchase", "don't need", "financial reasons" + ] + + reason_lower = return_request.return_reason.lower() + + # Check for valid reasons + is_valid = any(valid_reason in reason_lower for valid_reason in valid_reasons) + + # Check for invalid reasons + if any(invalid_reason in reason_lower for invalid_reason in invalid_reasons): + is_valid = False + + # Calculate confidence based on keyword matching + confidence = 0.8 if is_valid else 0.7 + + validation_reason = ( + "Return reason matches company policy for valid returns" if is_valid + else "Return reason does not meet company return policy criteria" + ) + + return ReturnValidationResult( + is_valid=is_valid, + reason=validation_reason, + confidence_score=confidence, + order_id=return_request.order_id + ) + + +def main(return_request_data: dict): + """Main function to run the order return validation agent""" + + agent = Agent( + name="Order Return Validation Specialist", + instructions=""" + You are an expert order return validation specialist. Your job is to: + 1. Analyze customer return requests carefully + 2. Determine if the return reason is valid according to company policy + 3. Provide clear reasoning for your decision + 4. Be fair but firm in applying policy guidelines + + Valid return reasons typically include: + - Defective or broken products + - Wrong item shipped + - Damaged during shipping + - Product not as described + - Quality issues + - Size/fit problems (for applicable items) + + Invalid return reasons typically include: + - Changed mind without valid cause + - Found item cheaper elsewhere + - Buyer's remorse + - Financial hardship + - General dissatisfaction without specific issue + + Always use the validate_return_reason tool to check the policy compliance. + Based on the tool result, clearly state if the return is VALID or INVALID. + End your response with either "Return is VALID" or "Return is INVALID". + """, + tools=[validate_return_reason], + ) + + # Convert dict to OrderReturnRequest + return_request = OrderReturnRequest(**return_request_data) + + user_message = f""" + Please validate this return request: + + Order ID: {return_request.order_id} + Customer ID: {return_request.customer_id} + Product Category: {return_request.product_category} + Purchase Date: {return_request.purchase_date} + Return Reason: {return_request.return_reason} + + Is this a valid return request according to our company policy? + """ + + result = Runner.run_sync(agent, user_message) + # Parse the agent's response to extract validation decision + agent_response = result.final_output + is_valid = "valid" in str(agent_response).lower() and "invalid" not in str(agent_response).lower() + + # Create a structured validation result + validation_result = { + "is_valid": is_valid, + "reason": "Parsed from agent response", + "confidence_score": 0.8 if is_valid else 0.7, + "order_id": return_request_data.get("order_id") + } + + return { + "agent_response": agent_response, + "validation_result": validation_result + } \ No newline at end of file diff --git a/samples-v2/openai_agents/order_returns/refund_processing.py b/samples-v2/openai_agents/order_returns/refund_processing.py new file mode 100644 index 00000000..4c0718c6 --- /dev/null +++ b/samples-v2/openai_agents/order_returns/refund_processing.py @@ -0,0 +1,110 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from __future__ import annotations + +from pydantic import BaseModel +from typing import Dict, Any + +from agents import Agent, Runner, function_tool + + +class RefundResult(BaseModel): + success: bool + refund_amount: float + transaction_id: str + refund_method: str + processing_time: str + + +@function_tool +def process_refund(order_id: str, refund_amount: float, customer_id: str) -> RefundResult: + """ + Process a refund for a validated return request. + This simulates calling payment processing systems. + """ + + # Simulate refund processing + import uuid + transaction_id = f"REF-{uuid.uuid4().hex[:8].upper()}" + + return RefundResult( + success=True, + refund_amount=refund_amount, + transaction_id=transaction_id, + refund_method="original_payment_method", + processing_time="3-5 business days" + ) + +@function_tool +def get_order_details(order_id: str) -> Dict[str, Any]: + """ + Retrieve order details for refund processing. + This simulates calling order management systems. + """ + + # Simulate order lookup + return { + "order_id": order_id, + "total_amount": 99.99, + "tax_amount": 7.99, + "shipping_cost": 5.99, + "product_cost": 86.01, + "currency": "USD", + "payment_method": "credit_card", + "order_status": "delivered" + } + + +def main(validation_data: dict): + """Main function to process refund for validated return""" + + agent = Agent( + name="Refund Processing Specialist", + instructions=""" + You are a refund processing specialist. Your job is to: + 1. Retrieve the order details for the validated return + 2. Calculate the appropriate refund amount (usually full product cost + tax) + 3. Process the refund through our payment systems + 4. Provide confirmation details to the customer + + Always retrieve order details first, then process the refund. + Be precise with refund amounts and provide clear transaction information. + End your response with "REFUND SUCCESS" if the refund is processed successfully. + """, + tools=[get_order_details, process_refund], + ) + + order_id = validation_data.get("order_id") + customer_id = validation_data.get("customer_id", "unknown") + + user_message = f""" + Process a refund for the validated return: + Order ID: {order_id} + Customer ID: {customer_id} + + Please: + 1. Get the order details + 2. Calculate the refund amount (product cost + tax, excluding shipping) + 3. Process the refund + 4. Provide confirmation details + """ + + result = Runner.run_sync(agent, user_message) + + # Parse the agent's response to extract refund status + agent_response = result.final_output + is_successful = "refund" in str(agent_response).lower() and "success" in str(agent_response).lower() + + # Create a structured refund result + refund_details = { + "success": is_successful, + "refund_amount": 93.00, # Product + Tax (86.01 + 7.99) + "transaction_id": f"REF-{hash(str(validation_data.get('order_id', ''))) % 10000:04d}", + "refund_method": "original_payment_method", + "processing_time": "3-5 business days" + } + + return { + "agent_response": agent_response, + "refund_details": refund_details + } \ No newline at end of file