diff --git a/samples-v2/openai_agents/ORDER_RETURNS.md b/samples-v2/openai_agents/ORDER_RETURNS.md new file mode 100644 index 00000000..1e80dabb --- /dev/null +++ b/samples-v2/openai_agents/ORDER_RETURNS.md @@ -0,0 +1,148 @@ +# Order Return Processing System + +An order return processing system built with Azure Functions and OpenAI Agents, demonstrating advanced orchestration patterns and multi-agent workflows. + +## Overview + +This system automatically processes customer return requests using AI agents to validate return reasons, process refunds, and route edge cases to human review. It showcases real-world business logic implementation with Azure Durable Functions. + +## Quick Start + +### 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" + }' +``` + +### Check Processing Status +```bash +# Use the statusQueryGetUri from the submission response +curl http://localhost:7071/runtime/webhooks/durabletask/instances/{instance_id} +``` + +## 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 + +``` +basic/ +├── order_return_validation.py # Validation agent +└── refund_processing.py # Refund processing agent + +order_return_orchestrators.py # Main orchestration logic +test_order_return.py # Testing examples and utilities +``` + +## Testing + +```bash +# Start the function app +func start + +# Test valid return (should auto-approve) +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 (should require human review) +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 using the response from above requests +# Look for "statusQueryGetUri" in the response and use that URL +curl {statusQueryGetUri_from_response} +``` \ No newline at end of file diff --git a/samples-v2/openai_agents/basic/order_return_validation.py b/samples-v2/openai_agents/basic/order_return_validation.py new file mode 100644 index 00000000..73bcf9a9 --- /dev/null +++ b/samples-v2/openai_agents/basic/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/basic/refund_processing.py b/samples-v2/openai_agents/basic/refund_processing.py new file mode 100644 index 00000000..4c0718c6 --- /dev/null +++ b/samples-v2/openai_agents/basic/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 diff --git a/samples-v2/openai_agents/function_app.py b/samples-v2/openai_agents/function_app.py index 1f52fde4..d9b8730b 100644 --- a/samples-v2/openai_agents/function_app.py +++ b/samples-v2/openai_agents/function_app.py @@ -10,6 +10,7 @@ from openai import AsyncAzureOpenAI from agents import set_default_openai_client +from order_return_orchestrators import register_order_return_orchestrators #region Regular Azure OpenAI setup @@ -36,6 +37,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 +48,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/order_return_orchestrators.py b/samples-v2/openai_agents/order_return_orchestrators.py new file mode 100644 index 00000000..4e81dd9b --- /dev/null +++ b/samples-v2/openai_agents/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 basic.order_return_validation + return_request = context.get_input() + return basic.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 basic.refund_processing + validation_data = context.get_input() + return basic.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