From b4a8e70eeec691bc78e11d067a39829b439750ce Mon Sep 17 00:00:00 2001 From: roshan Date: Mon, 25 Aug 2025 10:34:56 -0500 Subject: [PATCH 1/3] feat: add structured outputs documentation --- .../structured-outputs-examples.mdx | 1326 +++++++++++++++++ .../structured-outputs-quickstart.mdx | 713 +++++++++ fern/assistants/structured-outputs.mdx | 775 ++++++++++ fern/docs.yml | 10 + 4 files changed, 2824 insertions(+) create mode 100644 fern/assistants/structured-outputs-examples.mdx create mode 100644 fern/assistants/structured-outputs-quickstart.mdx create mode 100644 fern/assistants/structured-outputs.mdx diff --git a/fern/assistants/structured-outputs-examples.mdx b/fern/assistants/structured-outputs-examples.mdx new file mode 100644 index 000000000..d78bf1b86 --- /dev/null +++ b/fern/assistants/structured-outputs-examples.mdx @@ -0,0 +1,1326 @@ +--- +title: Structured outputs examples +subtitle: Real-world examples and templates for common use cases +slug: assistants/structured-outputs-examples +--- + +## Overview + +This page provides production-ready examples of structured outputs for common business scenarios. Each example includes the complete schema, configuration, and integration code. + +## Healthcare appointment booking + +Extract patient information and appointment preferences from healthcare calls. + + +```json title="Schema" +{ + "name": "Healthcare Appointment", + "type": "ai", + "description": "Extract patient and appointment information for medical scheduling", + "schema": { + "type": "object", + "properties": { + "patient": { + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "Patient's first name" + }, + "lastName": { + "type": "string", + "description": "Patient's last name" + }, + "dateOfBirth": { + "type": "string", + "format": "date", + "description": "Patient's date of birth (YYYY-MM-DD)" + }, + "phoneNumber": { + "type": "string", + "pattern": "^\\+?[1-9]\\d{1,14}$", + "description": "Patient's contact number" + }, + "insuranceProvider": { + "type": "string", + "description": "Insurance provider name" + }, + "memberID": { + "type": "string", + "description": "Insurance member ID" + } + }, + "required": ["firstName", "lastName", "phoneNumber"] + }, + "appointment": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["new-patient", "follow-up", "annual-checkup", "urgent-care", "specialist"], + "description": "Type of appointment" + }, + "department": { + "type": "string", + "enum": ["general", "cardiology", "dermatology", "orthopedics", "pediatrics", "obgyn"], + "description": "Medical department" + }, + "preferredDates": { + "type": "array", + "items": { + "type": "string", + "format": "date" + }, + "maxItems": 3, + "description": "Up to 3 preferred appointment dates" + }, + "preferredTimeSlot": { + "type": "string", + "enum": ["morning", "afternoon", "evening", "any"], + "description": "Preferred time of day" + }, + "symptoms": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of symptoms or reasons for visit" + }, + "urgency": { + "type": "string", + "enum": ["routine", "soon", "urgent"], + "description": "How urgent is the appointment" + } + }, + "required": ["type", "department", "urgency"] + }, + "additionalNotes": { + "type": "string", + "description": "Any additional notes or special requirements" + } + }, + "required": ["patient", "appointment"] + } +} +``` + +```javascript title="Integration Example" +// Process extracted healthcare appointment data +function processHealthcareAppointment(extractedData) { + const { patient, appointment } = extractedData; + + // Create patient record if new + if (appointment.type === 'new-patient') { + createPatientRecord(patient); + } + + // Check insurance eligibility + if (patient.insuranceProvider && patient.memberID) { + verifyInsurance(patient.insuranceProvider, patient.memberID); + } + + // Find available slots + const availableSlots = findAppointmentSlots({ + department: appointment.department, + dates: appointment.preferredDates, + timeSlot: appointment.preferredTimeSlot, + duration: appointment.type === 'new-patient' ? 60 : 30 + }); + + // Handle urgent cases + if (appointment.urgency === 'urgent') { + prioritizeAppointment(patient, appointment); + notifyOnCallStaff(appointment.department, appointment.symptoms); + } + + return { + patientId: patient.id, + appointmentOptions: availableSlots, + requiresPreAuth: checkPreAuthorization(appointment.type, patient.insuranceProvider) + }; +} +``` + + +## E-commerce order processing + +Capture order details, shipping information, and payment preferences. + + +```json title="Schema" +{ + "name": "E-commerce Order", + "type": "ai", + "description": "Extract complete order information from sales calls", + "schema": { + "type": "object", + "properties": { + "customer": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Customer full name" + }, + "email": { + "type": "string", + "format": "email", + "description": "Customer email for order confirmation" + }, + "phone": { + "type": "string", + "description": "Contact number" + }, + "loyaltyNumber": { + "type": "string", + "description": "Loyalty program member number if mentioned" + } + }, + "required": ["name", "email"] + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "productName": { + "type": "string", + "description": "Name or description of the product" + }, + "sku": { + "type": "string", + "description": "Product SKU if mentioned" + }, + "quantity": { + "type": "integer", + "minimum": 1, + "description": "Quantity ordered" + }, + "size": { + "type": "string", + "enum": ["XS", "S", "M", "L", "XL", "XXL", "custom"], + "description": "Size if applicable" + }, + "color": { + "type": "string", + "description": "Color preference" + }, + "customization": { + "type": "string", + "description": "Any customization requests" + } + }, + "required": ["productName", "quantity"] + }, + "minItems": 1, + "description": "List of items being ordered" + }, + "shipping": { + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": ["standard", "express", "overnight", "pickup"], + "description": "Shipping method" + }, + "address": { + "type": "object", + "properties": { + "street": { + "type": "string" + }, + "apartment": { + "type": "string", + "description": "Apartment or suite number" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string", + "pattern": "^[A-Z]{2}$" + }, + "zipCode": { + "type": "string", + "pattern": "^\\d{5}(-\\d{4})?$" + }, + "country": { + "type": "string", + "default": "USA" + } + }, + "required": ["street", "city", "state", "zipCode"] + }, + "instructions": { + "type": "string", + "description": "Special delivery instructions" + }, + "giftWrap": { + "type": "boolean", + "description": "Whether gift wrapping was requested" + }, + "giftMessage": { + "type": "string", + "description": "Gift message if applicable" + } + }, + "required": ["method", "address"] + }, + "payment": { + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": ["credit_card", "debit_card", "paypal", "apple_pay", "google_pay", "invoice"], + "description": "Payment method" + }, + "cardLastFour": { + "type": "string", + "pattern": "^\\d{4}$", + "description": "Last 4 digits of card if provided" + }, + "billingAddressSameAsShipping": { + "type": "boolean", + "description": "Whether billing address is same as shipping" + } + }, + "required": ["method"] + }, + "promotions": { + "type": "object", + "properties": { + "promoCode": { + "type": "string", + "description": "Promotional code mentioned" + }, + "referralSource": { + "type": "string", + "description": "How customer heard about us" + } + } + }, + "specialRequests": { + "type": "string", + "description": "Any special requests or notes" + } + }, + "required": ["customer", "items", "shipping"] + } +} +``` + +```python title="Integration Example" +def process_ecommerce_order(extracted_data): + """Process extracted e-commerce order data""" + + # Validate inventory + for item in extracted_data['items']: + stock = check_inventory( + product=item['productName'], + quantity=item['quantity'], + size=item.get('size'), + color=item.get('color') + ) + + if not stock['available']: + suggest_alternatives(item, stock['alternatives']) + + # Calculate pricing + subtotal = calculate_subtotal(extracted_data['items']) + shipping_cost = calculate_shipping( + method=extracted_data['shipping']['method'], + address=extracted_data['shipping']['address'], + items=extracted_data['items'] + ) + + # Apply promotions + discount = 0 + if promo_code := extracted_data.get('promotions', {}).get('promoCode'): + discount = apply_promo_code(promo_code, subtotal) + + # Apply loyalty benefits + if loyalty_num := extracted_data['customer'].get('loyaltyNumber'): + loyalty_discount = calculate_loyalty_discount(loyalty_num, subtotal) + discount += loyalty_discount + + # Create order + order = { + 'customer': extracted_data['customer'], + 'items': extracted_data['items'], + 'shipping': extracted_data['shipping'], + 'payment': extracted_data['payment'], + 'subtotal': subtotal, + 'shipping_cost': shipping_cost, + 'discount': discount, + 'total': subtotal + shipping_cost - discount, + 'status': 'pending_payment' + } + + # Handle gift orders + if extracted_data['shipping'].get('giftWrap'): + order['gift_options'] = { + 'wrap': True, + 'message': extracted_data['shipping'].get('giftMessage') + } + + return create_order_record(order) +``` + + +## Real estate lead qualification + +Qualify real estate leads and capture property preferences. + + +```json title="Schema" +{ + "name": "Real Estate Lead", + "type": "ai", + "description": "Qualify real estate leads and extract property preferences", + "schema": { + "type": "object", + "properties": { + "contact": { + "type": "object", + "properties": { + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "phone": { + "type": "string" + }, + "preferredContactMethod": { + "type": "string", + "enum": ["phone", "email", "text", "any"] + }, + "bestTimeToContact": { + "type": "string", + "enum": ["morning", "afternoon", "evening", "weekends", "any"] + } + }, + "required": ["firstName", "phone"] + }, + "propertySearch": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["buy", "rent", "sell", "invest"] + }, + "propertyType": { + "type": "array", + "items": { + "type": "string", + "enum": ["single-family", "condo", "townhouse", "multi-family", "commercial", "land"] + } + }, + "locations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "area": { + "type": "string", + "description": "Neighborhood, city, or region" + }, + "schools": { + "type": "boolean", + "description": "Important to be near good schools" + }, + "commute": { + "type": "string", + "description": "Commute requirements mentioned" + } + } + } + }, + "budget": { + "type": "object", + "properties": { + "min": { + "type": "number", + "minimum": 0 + }, + "max": { + "type": "number" + }, + "preApproved": { + "type": "boolean", + "description": "Whether they have mortgage pre-approval" + } + } + }, + "features": { + "type": "object", + "properties": { + "bedrooms": { + "type": "integer", + "minimum": 0 + }, + "bathrooms": { + "type": "number", + "minimum": 0 + }, + "squareFeet": { + "type": "integer", + "minimum": 0 + }, + "garage": { + "type": "boolean" + }, + "pool": { + "type": "boolean" + }, + "yard": { + "type": "boolean" + }, + "mustHaves": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of must-have features" + }, + "dealBreakers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of deal-breakers" + } + } + } + }, + "required": ["type", "propertyType", "budget"] + }, + "timeline": { + "type": "object", + "properties": { + "urgency": { + "type": "string", + "enum": ["immediate", "1-3months", "3-6months", "6-12months", "browsing"] + }, + "moveInDate": { + "type": "string", + "format": "date", + "description": "Target move-in date if mentioned" + }, + "reason": { + "type": "string", + "description": "Reason for moving/buying" + } + } + }, + "currentSituation": { + "type": "object", + "properties": { + "currentlyOwns": { + "type": "boolean", + "description": "Whether they currently own property" + }, + "needToSell": { + "type": "boolean", + "description": "Need to sell before buying" + }, + "firstTimeBuyer": { + "type": "boolean" + } + } + }, + "leadScore": { + "type": "object", + "properties": { + "motivation": { + "type": "string", + "enum": ["high", "medium", "low"], + "description": "Buyer/seller motivation level" + }, + "qualified": { + "type": "boolean", + "description": "Whether lead seems qualified" + }, + "followUpPriority": { + "type": "string", + "enum": ["hot", "warm", "cold"], + "description": "Follow-up priority" + } + } + } + }, + "required": ["contact", "propertySearch", "timeline"] + } +} +``` + +```javascript title="Integration Example" +// CRM integration for real estate leads +async function processRealEstateLead(extractedData) { + const { contact, propertySearch, timeline, currentSituation, leadScore } = extractedData; + + // Score the lead + const score = calculateLeadScore({ + budget: propertySearch.budget, + timeline: timeline.urgency, + preApproved: propertySearch.budget?.preApproved, + motivation: leadScore?.motivation, + currentlyOwns: currentSituation?.currentlyOwns + }); + + // Search matching properties + const matchingProperties = await searchProperties({ + type: propertySearch.propertyType, + locations: propertySearch.locations.map(l => l.area), + priceRange: { + min: propertySearch.budget.min, + max: propertySearch.budget.max + }, + features: { + minBedrooms: propertySearch.features?.bedrooms, + minBathrooms: propertySearch.features?.bathrooms, + mustHaves: propertySearch.features?.mustHaves + } + }); + + // Create lead in CRM + const leadId = await createCRMLead({ + contact, + score, + propertyPreferences: propertySearch, + timeline, + assignedAgent: selectBestAgent(propertySearch.locations, propertySearch.type) + }); + + // Set up automated follow-up + if (timeline.urgency === 'immediate' || leadScore?.followUpPriority === 'hot') { + scheduleImmediateCallback(leadId, contact); + sendPropertyMatches(contact.email, matchingProperties.slice(0, 5)); + } else { + scheduleDripCampaign(leadId, timeline.urgency); + } + + // Handle special situations + if (currentSituation?.firstTimeBuyer) { + sendFirstTimeBuyerGuide(contact.email); + } + + if (currentSituation?.needToSell) { + scheduleListingConsultation(leadId); + } + + return { + leadId, + score, + matchingProperties: matchingProperties.length, + assignedAgent: leadId.agent, + nextAction: determineNextAction(score, timeline) + }; +} +``` + + +## Insurance claim intake + +Capture insurance claim details and incident information. + + +```json title="Schema" +{ + "name": "Insurance Claim", + "type": "ai", + "description": "Extract insurance claim information and incident details", + "schema": { + "type": "object", + "properties": { + "policyholder": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "policyNumber": { + "type": "string", + "description": "Insurance policy number" + }, + "dateOfBirth": { + "type": "string", + "format": "date" + }, + "contactPhone": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "required": ["name", "policyNumber"] + }, + "incident": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["auto", "property", "theft", "injury", "liability", "other"] + }, + "date": { + "type": "string", + "format": "date", + "description": "Date of incident" + }, + "time": { + "type": "string", + "format": "time", + "description": "Approximate time of incident" + }, + "location": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "zipCode": { + "type": "string" + } + } + }, + "description": { + "type": "string", + "description": "Detailed description of what happened" + }, + "policeReportFiled": { + "type": "boolean" + }, + "policeReportNumber": { + "type": "string", + "description": "Police report number if available" + } + }, + "required": ["type", "date", "description"] + }, + "damages": { + "type": "object", + "properties": { + "propertyDamage": { + "type": "array", + "items": { + "type": "object", + "properties": { + "item": { + "type": "string", + "description": "Damaged item or property" + }, + "estimatedValue": { + "type": "number", + "description": "Estimated value or repair cost" + }, + "description": { + "type": "string", + "description": "Description of damage" + } + } + } + }, + "injuries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "person": { + "type": "string", + "description": "Injured person's name" + }, + "relationship": { + "type": "string", + "enum": ["self", "family", "passenger", "pedestrian", "other-driver", "other"], + "description": "Relationship to policyholder" + }, + "injuryDescription": { + "type": "string" + }, + "medicalTreatment": { + "type": "boolean", + "description": "Whether medical treatment was received" + }, + "hospital": { + "type": "string", + "description": "Hospital or clinic name if treated" + } + } + } + }, + "estimatedTotalLoss": { + "type": "number", + "description": "Total estimated loss amount" + } + } + }, + "otherParties": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "role": { + "type": "string", + "enum": ["other-driver", "witness", "property-owner", "passenger"], + "description": "Role in incident" + }, + "contactInfo": { + "type": "string", + "description": "Phone or email" + }, + "insuranceCompany": { + "type": "string", + "description": "Their insurance company if known" + }, + "policyNumber": { + "type": "string", + "description": "Their policy number if known" + } + } + } + }, + "documentation": { + "type": "object", + "properties": { + "photosAvailable": { + "type": "boolean" + }, + "receiptsAvailable": { + "type": "boolean" + }, + "witnessStatements": { + "type": "boolean" + } + } + }, + "urgency": { + "type": "string", + "enum": ["emergency", "urgent", "standard"], + "description": "Claim urgency level" + } + }, + "required": ["policyholder", "incident"] + } +} +``` + +```python title="Integration Example" +def process_insurance_claim(extracted_data): + """Process insurance claim intake""" + + # Verify policy + policy = verify_policy( + policy_number=extracted_data['policyholder']['policyNumber'], + name=extracted_data['policyholder']['name'], + dob=extracted_data['policyholder'].get('dateOfBirth') + ) + + if not policy['active']: + return { + 'status': 'rejected', + 'reason': 'Policy not active', + 'action': 'transfer_to_agent' + } + + # Check coverage + coverage = check_coverage( + policy_type=policy['type'], + incident_type=extracted_data['incident']['type'], + incident_date=extracted_data['incident']['date'] + ) + + # Create claim + claim = { + 'claimNumber': generate_claim_number(), + 'policyholder': extracted_data['policyholder'], + 'incident': extracted_data['incident'], + 'damages': extracted_data.get('damages', {}), + 'status': 'intake_complete', + 'coveredAmount': coverage['estimated_coverage'], + 'deductible': coverage['deductible'] + } + + # Handle injuries + if injuries := extracted_data.get('damages', {}).get('injuries'): + for injury in injuries: + if injury.get('medicalTreatment'): + claim['requires_medical_review'] = True + schedule_medical_review(claim['claimNumber'], injury) + + # Handle emergency situations + if extracted_data.get('urgency') == 'emergency': + claim['priority'] = 'high' + dispatch_emergency_adjuster(claim) + + # Arrange temporary solutions + if extracted_data['incident']['type'] == 'property': + arrange_emergency_repairs(extracted_data['incident']['location']) + elif extracted_data['incident']['type'] == 'auto': + arrange_rental_car(extracted_data['policyholder']) + + # Documentation requirements + required_docs = determine_required_documentation( + incident_type=extracted_data['incident']['type'], + damages=extracted_data.get('damages'), + amount=extracted_data.get('damages', {}).get('estimatedTotalLoss', 0) + ) + + # Create claim record + claim_id = save_claim(claim) + + # Send confirmation + send_claim_confirmation( + email=extracted_data['policyholder']['email'], + claim_number=claim['claimNumber'], + next_steps=required_docs, + adjuster_contact=claim.get('assigned_adjuster') + ) + + return { + 'claimNumber': claim['claimNumber'], + 'status': 'accepted', + 'estimatedProcessingTime': estimate_processing_time(claim), + 'requiredDocuments': required_docs, + 'nextSteps': generate_next_steps(claim) + } +``` + + +## Financial services application + +Process loan or credit applications with financial information. + + +```json title="Schema" +{ + "name": "Financial Application", + "type": "ai", + "description": "Extract loan or credit application information", + "schema": { + "type": "object", + "properties": { + "applicant": { + "type": "object", + "properties": { + "personalInfo": { + "type": "object", + "properties": { + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "ssn": { + "type": "string", + "pattern": "^\\d{3}-\\d{2}-\\d{4}$", + "description": "Social Security Number (XXX-XX-XXXX)" + }, + "dateOfBirth": { + "type": "string", + "format": "date" + }, + "email": { + "type": "string", + "format": "email" + }, + "phone": { + "type": "string" + }, + "currentAddress": { + "type": "object", + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "zipCode": { + "type": "string" + }, + "yearsAtAddress": { + "type": "number" + }, + "rentOrOwn": { + "type": "string", + "enum": ["rent", "own", "other"] + } + } + } + }, + "required": ["firstName", "lastName", "dateOfBirth"] + }, + "employment": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["employed", "self-employed", "unemployed", "retired", "student"] + }, + "employer": { + "type": "string", + "description": "Employer name" + }, + "position": { + "type": "string", + "description": "Job title" + }, + "yearsEmployed": { + "type": "number" + }, + "annualIncome": { + "type": "number", + "minimum": 0, + "description": "Annual gross income" + }, + "otherIncome": { + "type": "number", + "description": "Other income sources" + }, + "incomeVerifiable": { + "type": "boolean", + "description": "Can provide income verification" + } + }, + "required": ["status", "annualIncome"] + }, + "financial": { + "type": "object", + "properties": { + "creditScore": { + "type": "integer", + "minimum": 300, + "maximum": 850, + "description": "Self-reported credit score" + }, + "monthlyDebt": { + "type": "number", + "description": "Total monthly debt payments" + }, + "bankruptcyHistory": { + "type": "boolean", + "description": "Any bankruptcy in past 7 years" + }, + "existingAccounts": { + "type": "array", + "items": { + "type": "string", + "enum": ["checking", "savings", "credit-card", "mortgage", "auto-loan", "student-loan"] + }, + "description": "Existing accounts with institution" + } + } + } + } + }, + "loanDetails": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["personal", "auto", "mortgage", "home-equity", "business", "student"], + "description": "Type of loan" + }, + "amount": { + "type": "number", + "minimum": 0, + "description": "Requested loan amount" + }, + "purpose": { + "type": "string", + "description": "Purpose of the loan" + }, + "term": { + "type": "integer", + "description": "Desired loan term in months" + }, + "collateral": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["vehicle", "property", "savings", "none"], + "description": "Type of collateral" + }, + "value": { + "type": "number", + "description": "Estimated value of collateral" + }, + "description": { + "type": "string", + "description": "Description of collateral" + } + } + } + }, + "required": ["type", "amount", "purpose"] + }, + "coApplicant": { + "type": "object", + "properties": { + "hasCoApplicant": { + "type": "boolean" + }, + "relationship": { + "type": "string", + "enum": ["spouse", "partner", "family", "business-partner", "other"] + }, + "name": { + "type": "string" + }, + "income": { + "type": "number" + } + } + }, + "preferences": { + "type": "object", + "properties": { + "preferredRate": { + "type": "string", + "enum": ["fixed", "variable", "no-preference"] + }, + "automaticPayment": { + "type": "boolean", + "description": "Interested in automatic payment" + }, + "paperless": { + "type": "boolean", + "description": "Prefers paperless statements" + } + } + } + }, + "required": ["applicant", "loanDetails"] + } +} +``` + +```javascript title="Integration Example" +async function processFinancialApplication(extractedData) { + const { applicant, loanDetails, coApplicant } = extractedData; + + // Initial eligibility check + const eligibility = await checkEligibility({ + creditScore: applicant.financial?.creditScore, + income: applicant.employment.annualIncome, + employmentStatus: applicant.employment.status, + loanAmount: loanDetails.amount, + loanType: loanDetails.type + }); + + if (!eligibility.qualified) { + return { + status: 'declined', + reason: eligibility.reason, + alternatives: suggestAlternatives(eligibility, loanDetails) + }; + } + + // Calculate debt-to-income ratio + const monthlyIncome = (applicant.employment.annualIncome + + (applicant.employment.otherIncome || 0)) / 12; + const dti = (applicant.financial?.monthlyDebt || 0) / monthlyIncome; + + // Risk assessment + const riskScore = calculateRiskScore({ + creditScore: applicant.financial?.creditScore, + dti, + employmentYears: applicant.employment.yearsEmployed, + bankruptcyHistory: applicant.financial?.bankruptcyHistory, + collateral: loanDetails.collateral, + existingCustomer: applicant.financial?.existingAccounts?.length > 0 + }); + + // Determine loan terms + const terms = generateLoanTerms({ + loanType: loanDetails.type, + amount: loanDetails.amount, + term: loanDetails.term, + riskScore, + preferredRate: extractedData.preferences?.preferredRate, + hasCollateral: loanDetails.collateral?.type !== 'none' + }); + + // Create application + const applicationId = await createApplication({ + applicant, + loanDetails, + coApplicant: coApplicant?.hasCoApplicant ? coApplicant : null, + eligibility, + riskScore, + proposedTerms: terms, + status: determineInitialStatus(riskScore, loanDetails.amount) + }); + + // Handle co-applicant + if (coApplicant?.hasCoApplicant) { + await requestCoApplicantInfo(applicationId, coApplicant); + } + + // Schedule next steps + if (riskScore.requiresManualReview) { + await assignToUnderwriter(applicationId, riskScore.reviewReasons); + } else if (riskScore.preApproved) { + await sendPreApprovalLetter(applicant.personalInfo.email, terms); + } + + // Set up document collection + const requiredDocs = determineRequiredDocuments({ + loanType: loanDetails.type, + amount: loanDetails.amount, + employmentStatus: applicant.employment.status, + incomeVerifiable: applicant.employment.incomeVerifiable + }); + + await sendDocumentRequest(applicant.personalInfo.email, requiredDocs); + + return { + applicationId, + status: terms.status, + preApproved: riskScore.preApproved, + estimatedRate: terms.rate, + monthlyPayment: terms.monthlyPayment, + requiredDocuments: requiredDocs, + nextSteps: generateApplicationNextSteps(riskScore, requiredDocs) + }; +} +``` + + +## Best practices for complex schemas + + + + Break complex schemas into reusable object definitions for maintainability + + + + Start with essential fields as required, make detailed fields optional + + + + Add descriptions to every field to help AI understand context + + + + Use constraints for data quality but avoid being too restrictive + + + +## Testing recommendations + +### Test scenarios + +Always test your structured outputs with these scenarios: + +1. **Complete information** - All fields mentioned clearly +2. **Partial information** - Some required fields missing +3. **Ambiguous data** - Unclear or conflicting information +4. **Edge cases** - Boundary values, special characters +5. **Real conversations** - Actual call recordings or transcripts + +### Monitoring checklist + +Track these metrics for production deployments: + +- Extraction success rate per field +- Average extraction time +- Token usage and costs +- Schema validation failures +- Most commonly missing fields + +## Integration patterns + +### Webhook processing + +```javascript +// Robust webhook handler with error handling +app.post('/vapi/structured-output', async (req, res) => { + try { + const { type, call } = req.body; + + if (type !== 'call.ended') { + return res.status(200).send('OK'); + } + + const outputs = call.artifact?.structuredOutputs || {}; + + for (const [outputId, data] of Object.entries(outputs)) { + if (!data.result) { + console.error(`Extraction failed for ${data.name}`); + await notifyExtractionFailure(call.id, data.name); + continue; + } + + try { + await processExtractedData(data.name, data.result); + } catch (error) { + console.error(`Processing failed for ${data.name}:`, error); + await queueForRetry(call.id, outputId, data); + } + } + + res.status(200).send('OK'); + } catch (error) { + console.error('Webhook processing error:', error); + res.status(500).send('Internal error'); + } +}); +``` + +### Batch processing + +```python +def batch_process_extractions(call_ids): + """Process multiple calls in batch""" + + results = [] + + for call_id in call_ids: + call = vapi.calls.get(call_id) + outputs = call.artifact.get('structuredOutputs', {}) + + for output_id, data in outputs.items(): + if data['result']: + results.append({ + 'call_id': call_id, + 'output_name': data['name'], + 'data': data['result'], + 'extracted_at': call.ended_at + }) + + # Bulk insert to database + if results: + bulk_insert_extracted_data(results) + + return len(results) +``` + +## Related resources + +- [Structured outputs overview](/assistants/structured-outputs) - Main documentation +- [Quickstart guide](/assistants/structured-outputs-quickstart) - Get started quickly +- [API reference](/api-reference#structured-output) - Complete API documentation +- [JSON Schema specification](https://json-schema.org/) - JSON Schema standard \ No newline at end of file diff --git a/fern/assistants/structured-outputs-quickstart.mdx b/fern/assistants/structured-outputs-quickstart.mdx new file mode 100644 index 000000000..9dcb85570 --- /dev/null +++ b/fern/assistants/structured-outputs-quickstart.mdx @@ -0,0 +1,713 @@ +--- +title: Structured outputs quickstart +subtitle: Get started with structured data extraction in 5 minutes +slug: assistants/structured-outputs-quickstart +--- + +## Overview + +This quickstart guide will help you set up structured outputs to automatically extract customer information from phone calls. In just a few minutes, you'll create a structured output, link it to an assistant, and test data extraction. + +## What you'll build + +A customer support assistant that automatically extracts: +- Customer name and contact details +- Issue description and priority +- Requested follow-up actions + +## Prerequisites + + + + Sign up at [dashboard.vapi.ai](https://dashboard.vapi.ai) + + + Get your API key from the Dashboard settings + + + +## Step 1: Create your structured output + +First, define what information you want to extract using a JSON Schema: + + +```bash title="cURL" +curl -X POST https://api.vapi.ai/structured-output \ + -H "Authorization: Bearer $VAPI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Support Ticket", + "type": "ai", + "description": "Extract support ticket information from customer calls", + "schema": { + "type": "object", + "properties": { + "customer": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Customer full name" + }, + "email": { + "type": "string", + "format": "email", + "description": "Customer email address" + }, + "phone": { + "type": "string", + "description": "Customer phone number" + } + }, + "required": ["name"] + }, + "issue": { + "type": "object", + "properties": { + "description": { + "type": "string", + "description": "Description of the customer issue" + }, + "category": { + "type": "string", + "enum": ["billing", "technical", "general", "complaint"], + "description": "Issue category" + }, + "priority": { + "type": "string", + "enum": ["low", "medium", "high", "urgent"], + "description": "Issue priority level" + } + }, + "required": ["description", "category"] + }, + "followUp": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Whether follow-up is needed" + }, + "method": { + "type": "string", + "enum": ["email", "phone", "none"], + "description": "Preferred follow-up method" + }, + "notes": { + "type": "string", + "description": "Additional notes for follow-up" + } + } + } + }, + "required": ["customer", "issue"] + } + }' +``` + +```javascript title="Node.js" +const response = await fetch('https://api.vapi.ai/structured-output', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${process.env.VAPI_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: "Support Ticket", + type: "ai", + description: "Extract support ticket information from customer calls", + schema: { + type: "object", + properties: { + customer: { + type: "object", + properties: { + name: { + type: "string", + description: "Customer full name" + }, + email: { + type: "string", + format: "email", + description: "Customer email address" + }, + phone: { + type: "string", + description: "Customer phone number" + } + }, + required: ["name"] + }, + issue: { + type: "object", + properties: { + description: { + type: "string", + description: "Description of the customer issue" + }, + category: { + type: "string", + enum: ["billing", "technical", "general", "complaint"], + description: "Issue category" + }, + priority: { + type: "string", + enum: ["low", "medium", "high", "urgent"], + description: "Issue priority level" + } + }, + required: ["description", "category"] + }, + followUp: { + type: "object", + properties: { + required: { + type: "boolean", + description: "Whether follow-up is needed" + }, + method: { + type: "string", + enum: ["email", "phone", "none"], + description: "Preferred follow-up method" + }, + notes: { + type: "string", + description: "Additional notes for follow-up" + } + } + } + }, + required: ["customer", "issue"] + } + }) +}); + +const structuredOutput = await response.json(); +console.log('Created structured output:', structuredOutput.id); +// Save this ID - you'll need it in the next step +``` + +```python title="Python" +import requests +import os + +response = requests.post( + 'https://api.vapi.ai/structured-output', + headers={ + 'Authorization': f'Bearer {os.environ["VAPI_API_KEY"]}', + 'Content-Type': 'application/json' + }, + json={ + "name": "Support Ticket", + "type": "ai", + "description": "Extract support ticket information from customer calls", + "schema": { + "type": "object", + "properties": { + "customer": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Customer full name" + }, + "email": { + "type": "string", + "format": "email", + "description": "Customer email address" + }, + "phone": { + "type": "string", + "description": "Customer phone number" + } + }, + "required": ["name"] + }, + "issue": { + "type": "object", + "properties": { + "description": { + "type": "string", + "description": "Description of the customer issue" + }, + "category": { + "type": "string", + "enum": ["billing", "technical", "general", "complaint"], + "description": "Issue category" + }, + "priority": { + "type": "string", + "enum": ["low", "medium", "high", "urgent"], + "description": "Issue priority level" + } + }, + "required": ["description", "category"] + }, + "followUp": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "description": "Whether follow-up is needed" + }, + "method": { + "type": "string", + "enum": ["email", "phone", "none"], + "description": "Preferred follow-up method" + }, + "notes": { + "type": "string", + "description": "Additional notes for follow-up" + } + } + } + }, + "required": ["customer", "issue"] + } + } +) + +structured_output = response.json() +print(f'Created structured output: {structured_output["id"]}') +# Save this ID - you'll need it in the next step +``` + + + +Save the returned `id` from the response - you'll need it to link to your assistant. + + +## Step 2: Create an assistant with structured outputs + +Now create an assistant that uses your structured output: + + +```bash title="cURL" +curl -X POST https://api.vapi.ai/assistant \ + -H "Authorization: Bearer $VAPI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Customer Support Agent", + "firstMessage": "Hello! I'\''m here to help you with your support request. Can you please tell me your name and describe the issue you'\''re experiencing?", + "model": { + "provider": "openai", + "model": "gpt-4-turbo-preview", + "messages": [ + { + "role": "system", + "content": "You are a helpful customer support agent. Gather the customer'\''s information and understand their issue. Be empathetic and professional." + } + ] + }, + "voice": { + "provider": "vapi", + "voiceId": "jennifer" + }, + "artifactPlan": { + "structuredOutputIds": ["YOUR_STRUCTURED_OUTPUT_ID_HERE"] + } + }' +``` + +```javascript title="Node.js" +const assistant = await fetch('https://api.vapi.ai/assistant', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${process.env.VAPI_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: "Customer Support Agent", + firstMessage: "Hello! I'm here to help you with your support request. Can you please tell me your name and describe the issue you're experiencing?", + model: { + provider: "openai", + model: "gpt-4-turbo-preview", + messages: [ + { + role: "system", + content: "You are a helpful customer support agent. Gather the customer's information and understand their issue. Be empathetic and professional." + } + ] + }, + voice: { + provider: "vapi", + voiceId: "jennifer" + }, + artifactPlan: { + structuredOutputIds: [structuredOutput.id] // Use the ID from step 1 + } + }) +}).then(res => res.json()); + +console.log('Created assistant:', assistant.id); +``` + +```python title="Python" +assistant_response = requests.post( + 'https://api.vapi.ai/assistant', + headers={ + 'Authorization': f'Bearer {os.environ["VAPI_API_KEY"]}', + 'Content-Type': 'application/json' + }, + json={ + "name": "Customer Support Agent", + "firstMessage": "Hello! I'm here to help you with your support request. Can you please tell me your name and describe the issue you're experiencing?", + "model": { + "provider": "openai", + "model": "gpt-4-turbo-preview", + "messages": [ + { + "role": "system", + "content": "You are a helpful customer support agent. Gather the customer's information and understand their issue. Be empathetic and professional." + } + ] + }, + "voice": { + "provider": "vapi", + "voiceId": "jennifer" + }, + "artifactPlan": { + "structuredOutputIds": [structured_output["id"]] # Use the ID from step 1 + } + } +) + +assistant = assistant_response.json() +print(f'Created assistant: {assistant["id"]}') +``` + + +## Step 3: Test with a phone call + +Make a test call to your assistant: + + +```bash title="cURL" +curl -X POST https://api.vapi.ai/call \ + -H "Authorization: Bearer $VAPI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "assistantId": "YOUR_ASSISTANT_ID_HERE", + "customer": { + "number": "+1234567890" + } + }' +``` + +```javascript title="Node.js" +const call = await fetch('https://api.vapi.ai/call', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${process.env.VAPI_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + assistantId: assistant.id, + customer: { + number: "+1234567890" // Replace with your phone number + } + }) +}).then(res => res.json()); + +console.log('Call initiated:', call.id); +``` + +```python title="Python" +call_response = requests.post( + 'https://api.vapi.ai/call', + headers={ + 'Authorization': f'Bearer {os.environ["VAPI_API_KEY"]}', + 'Content-Type': 'application/json' + }, + json={ + "assistantId": assistant["id"], + "customer": { + "number": "+1234567890" # Replace with your phone number + } + } +) + +call = call_response.json() +print(f'Call initiated: {call["id"]}') +``` + + + +During the call, try saying something like: "Hi, my name is John Smith. My email is john@example.com. I'm having trouble logging into my account - it keeps showing an error message. This is pretty urgent for me." + + +## Step 4: Retrieve extracted data + +After the call ends, retrieve the extracted information: + + +```bash title="cURL" +curl -X GET "https://api.vapi.ai/call/YOUR_CALL_ID_HERE" \ + -H "Authorization: Bearer $VAPI_API_KEY" +``` + +```javascript title="Node.js" +// Wait a few seconds after call ends for processing +setTimeout(async () => { + const callData = await fetch(`https://api.vapi.ai/call/${call.id}`, { + headers: { + 'Authorization': `Bearer ${process.env.VAPI_API_KEY}` + } + }).then(res => res.json()); + + const outputs = callData.artifact?.structuredOutputs; + + if (outputs) { + Object.entries(outputs).forEach(([outputId, data]) => { + console.log('Extracted Support Ticket:'); + console.log(JSON.stringify(data.result, null, 2)); + }); + } +}, 5000); +``` + +```python title="Python" +import time +import json + +# Wait a few seconds after call ends for processing +time.sleep(5) + +call_data = requests.get( + f'https://api.vapi.ai/call/{call["id"]}', + headers={ + 'Authorization': f'Bearer {os.environ["VAPI_API_KEY"]}' + } +).json() + +outputs = call_data.get('artifact', {}).get('structuredOutputs', {}) + +for output_id, data in outputs.items(): + print('Extracted Support Ticket:') + print(json.dumps(data['result'], indent=2)) +``` + + +### Expected output + +You should see extracted data like this: + +```json +{ + "customer": { + "name": "John Smith", + "email": "john@example.com", + "phone": "+1234567890" + }, + "issue": { + "description": "Unable to login to account, receiving error message", + "category": "technical", + "priority": "urgent" + }, + "followUp": { + "required": true, + "method": "email", + "notes": "Customer needs immediate assistance with login issue" + } +} +``` + +## Step 5: Set up webhook (optional) + +To automatically receive extracted data when calls end, set up a webhook: + + +```javascript title="Express.js webhook handler" +const express = require('express'); +const app = express(); + +app.use(express.json()); + +app.post('/vapi/webhook', (req, res) => { + const { type, call } = req.body; + + if (type === 'call.ended') { + const outputs = call.artifact?.structuredOutputs; + + if (outputs) { + Object.entries(outputs).forEach(([outputId, data]) => { + if (data.result) { + // Process the extracted support ticket + console.log('New support ticket:', data.result); + + // Example: Create ticket in your system + createSupportTicket({ + customer: data.result.customer, + issue: data.result.issue, + priority: data.result.issue.priority, + followUp: data.result.followUp + }); + } + }); + } + } + + res.status(200).send('OK'); +}); + +function createSupportTicket(ticketData) { + // Your ticket creation logic here + console.log('Creating ticket in system:', ticketData); +} + +app.listen(3000, () => { + console.log('Webhook server running on port 3000'); +}); +``` + +```python title="Flask webhook handler" +from flask import Flask, request, jsonify + +app = Flask(__name__) + +@app.route('/vapi/webhook', methods=['POST']) +def vapi_webhook(): + data = request.json + + if data.get('type') == 'call.ended': + call = data.get('call', {}) + outputs = call.get('artifact', {}).get('structuredOutputs', {}) + + for output_id, output_data in outputs.items(): + if output_data.get('result'): + # Process the extracted support ticket + print('New support ticket:', output_data['result']) + + # Example: Create ticket in your system + create_support_ticket({ + 'customer': output_data['result']['customer'], + 'issue': output_data['result']['issue'], + 'priority': output_data['result']['issue']['priority'], + 'followUp': output_data['result']['followUp'] + }) + + return jsonify({'status': 'ok'}), 200 + +def create_support_ticket(ticket_data): + # Your ticket creation logic here + print('Creating ticket in system:', ticket_data) + +if __name__ == '__main__': + app.run(port=3000) +``` + + +Then update your assistant with the webhook URL: + +```bash +curl -X PATCH "https://api.vapi.ai/assistant/YOUR_ASSISTANT_ID" \ + -H "Authorization: Bearer $VAPI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "serverUrl": "https://your-domain.com/vapi/webhook" + }' +``` + +## Next steps + + + + Learn about different data types and validation options + + + + Configure different AI models for extraction + + + + See complex real-world extraction scenarios + + + + Complete API documentation for structured outputs + + + +## Common patterns + +### Multiple extractions + +You can attach multiple structured outputs to extract different types of data: + +```javascript +{ + artifactPlan: { + structuredOutputIds: [ + customerInfoId, // Extract customer details + appointmentId, // Extract appointment requests + feedbackId // Extract satisfaction feedback + ] + } +} +``` + +### Conditional extraction + +Use conditional logic in your schema to handle different scenarios: + +```json +{ + "if": { + "properties": { + "requestType": {"const": "appointment"} + } + }, + "then": { + "required": ["preferredDate", "preferredTime"] + } +} +``` + +### Validation patterns + +Common validation patterns for reliable extraction: + +```json +{ + "email": { + "type": "string", + "format": "email", + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" + }, + "phone": { + "type": "string", + "pattern": "^\\+?[1-9]\\d{1,14}$" + }, + "zipCode": { + "type": "string", + "pattern": "^\\d{5}(-\\d{4})?$" + } +} +``` + +## Tips for success + + +**Best practices for reliable extraction:** +- Start with required fields only for critical data +- Use enums for categorical data to ensure consistency +- Add descriptions to help the AI understand context +- Test with real conversations before production use +- Monitor extraction success rates and iterate on schemas + + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| No data extracted | Verify the information was mentioned in the call and check schema validity | +| Partial extraction | Make non-critical fields optional and simplify nested structures | +| Incorrect values | Add more specific validation patterns and field descriptions | +| Extraction fails | Check API logs, verify assistant configuration, and test with simpler schema | + +## Get help + +Need assistance? We're here to help: +- [API Documentation](/api-reference) +- [Discord Community](https://discord.gg/pUFNcf2WmH) +- [Support](mailto:support@vapi.ai) \ No newline at end of file diff --git a/fern/assistants/structured-outputs.mdx b/fern/assistants/structured-outputs.mdx new file mode 100644 index 000000000..b474f1199 --- /dev/null +++ b/fern/assistants/structured-outputs.mdx @@ -0,0 +1,775 @@ +--- +title: Structured outputs +subtitle: Extract structured data from conversations using AI-powered analysis +slug: assistants/structured-outputs +--- + +## Overview + +Structured outputs enable automatic extraction of specific information from voice conversations in a structured format. Define your data requirements using JSON Schema, and we will identify and extract that information from your calls. + +**Key benefits:** +- Extract customer information, appointments, and orders automatically +- Validate data with JSON Schema constraints +- Use any AI model for extraction (OpenAI, Anthropic, Google, Azure) +- Reuse extraction definitions across multiple assistants + +## How it works + + + + Create a JSON Schema that describes the data you want to extract + + + Use the API to create a reusable structured output definition + + + Connect the structured output to one or more assistants or workflows + + + Data is automatically extracted after each call and stored in call artifacts + + + +## Quick start + +### Create a structured output + + +```typescript title="TypeScript (Server SDK)" +import { Vapi } from '@vapi-ai/server-sdk'; + +const vapi = new Vapi({ apiKey: process.env.VAPI_API_KEY }); + +const structuredOutput = await vapi.structuredOutputs.create({ + name: "Customer Info", + type: "ai", + description: "Extract customer contact information", + schema: { + type: "object", + properties: { + firstName: { + type: "string", + description: "Customer's first name" + }, + lastName: { + type: "string", + description: "Customer's last name" + }, + email: { + type: "string", + format: "email", + description: "Customer's email address" + }, + phone: { + type: "string", + pattern: "^\\+?[1-9]\\d{1,14}$", + description: "Phone number in E.164 format" + } + }, + required: ["firstName", "lastName"] + } +}); + +console.log('Created structured output:', structuredOutput.id); +``` + +```python title="Python (Server SDK)" +from vapi_python import Vapi + +vapi = Vapi(api_key=os.environ['VAPI_API_KEY']) + +structured_output = vapi.structured_outputs.create( + name="Customer Info", + type="ai", + description="Extract customer contact information", + schema={ + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "Customer's first name" + }, + "lastName": { + "type": "string", + "description": "Customer's last name" + }, + "email": { + "type": "string", + "format": "email", + "description": "Customer's email address" + }, + "phone": { + "type": "string", + "pattern": "^\\+?[1-9]\\d{1,14}$", + "description": "Phone number in E.164 format" + } + }, + "required": ["firstName", "lastName"] + } +) + +print(f"Created structured output: {structured_output.id}") +``` + +```bash title="cURL" +curl -X POST https://api.vapi.ai/structured-output \ + -H "Authorization: Bearer $VAPI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Customer Info", + "type": "ai", + "description": "Extract customer contact information", + "schema": { + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "Customer'\''s first name" + }, + "lastName": { + "type": "string", + "description": "Customer'\''s last name" + }, + "email": { + "type": "string", + "format": "email", + "description": "Customer'\''s email address" + }, + "phone": { + "type": "string", + "pattern": "^\\+?[1-9]\\d{1,14}$", + "description": "Phone number in E.164 format" + } + }, + "required": ["firstName", "lastName"] + } + }' +``` + + +### Link to an assistant + +Add the structured output ID to your assistant's configuration: + + +```typescript title="TypeScript (Server SDK)" +const assistant = await vapi.assistants.create({ + name: "Customer Support Agent", + // ... other assistant configuration + artifactPlan: { + structuredOutputIds: [structuredOutput.id] + } +}); +``` + +```python title="Python (Server SDK)" +assistant = vapi.assistants.create( + name="Customer Support Agent", + # ... other assistant configuration + artifact_plan={ + "structuredOutputIds": [structured_output.id] + } +) +``` + +```bash title="cURL" +curl -X POST https://api.vapi.ai/assistant \ + -H "Authorization: Bearer $VAPI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Customer Support Agent", + "artifactPlan": { + "structuredOutputIds": ["output-id-here"] + } + }' +``` + + +### Access extracted data + +After a call completes, retrieve the extracted data: + + +```typescript title="TypeScript (Server SDK)" +const call = await vapi.calls.get(callId); + +// Access structured outputs from call artifacts +const outputs = call.artifact?.structuredOutputs; + +if (outputs) { + for (const [outputId, data] of Object.entries(outputs)) { + console.log(`Output: ${data.name}`); + console.log(`Result:`, data.result); + + // Handle the extracted data + if (data.result) { + // Process successful extraction + const { firstName, lastName, email, phone } = data.result; + // ... save to database, send notifications, etc. + } + } +} +``` + +```python title="Python (Server SDK)" +call = vapi.calls.get(call_id) + +# Access structured outputs from call artifacts +outputs = call.artifact.get('structuredOutputs', {}) + +for output_id, data in outputs.items(): + print(f"Output: {data['name']}") + print(f"Result: {data['result']}") + + # Handle the extracted data + if data['result']: + # Process successful extraction + result = data['result'] + first_name = result.get('firstName') + last_name = result.get('lastName') + email = result.get('email') + phone = result.get('phone') + # ... save to database, send notifications, etc. +``` + +```javascript title="Webhook Response" +// In your webhook handler +app.post('/vapi/webhook', (req, res) => { + const { type, call } = req.body; + + if (type === 'call.ended') { + const outputs = call.artifact?.structuredOutputs; + + if (outputs) { + Object.entries(outputs).forEach(([outputId, data]) => { + console.log(`Extracted ${data.name}:`, data.result); + // Process the extracted data + }); + } + } + + res.status(200).send('OK'); +}); +``` + + +## Schema types + +### Primitive types + +Extract simple values directly: + + +```json title="String" +{ + "type": "string", + "minLength": 1, + "maxLength": 100, + "pattern": "^[A-Z][a-z]+$" +} +``` + +```json title="Number" +{ + "type": "number", + "minimum": 0, + "maximum": 100, + "multipleOf": 0.5 +} +``` + +```json title="Boolean" +{ + "type": "boolean", + "description": "Whether customer agreed to terms" +} +``` + +```json title="Enum" +{ + "type": "string", + "enum": ["small", "medium", "large", "extra-large"] +} +``` + + +### Object types + +Extract structured data with multiple fields: + +```json +{ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Full name" + }, + "age": { + "type": "integer", + "minimum": 0, + "maximum": 120 + }, + "email": { + "type": "string", + "format": "email" + } + }, + "required": ["name", "email"] +} +``` + +### Array types + +Extract lists of items: + +```json +{ + "type": "array", + "items": { + "type": "object", + "properties": { + "product": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1 + } + } + }, + "minItems": 1, + "maxItems": 10 +} +``` + +### Nested structures + +Extract complex hierarchical data: + +```json +{ + "type": "object", + "properties": { + "customer": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "contact": { + "type": "object", + "properties": { + "email": {"type": "string", "format": "email"}, + "phone": {"type": "string"} + } + } + } + }, + "order": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "sku": {"type": "string"}, + "quantity": {"type": "integer"} + } + } + } + } + } + } +} +``` + +## Validation features + +### String formats + +Vapi supports standard JSON Schema formats for validation: + +| Format | Description | Example | +|--------|-------------|---------| +| `email` | Email addresses | john@example.com | +| `date` | Date in YYYY-MM-DD | 2024-01-15 | +| `time` | Time in HH:MM:SS | 14:30:00 | +| `date-time` | ISO 8601 datetime | 2024-01-15T14:30:00Z | +| `uri` | Valid URI | https://example.com | +| `uuid` | UUID format | 123e4567-e89b-12d3-a456-426614174000 | + +### Pattern matching + +Use regular expressions for custom validation: + +```json +{ + "type": "string", + "pattern": "^[A-Z]{2}-\\d{6}$", + "description": "Order ID like US-123456" +} +``` + +### Conditional logic + +Use `if/then/else` for conditional requirements: + +```json +{ + "type": "object", + "properties": { + "serviceType": { + "type": "string", + "enum": ["emergency", "scheduled"] + }, + "appointmentTime": { + "type": "string", + "format": "date-time" + } + }, + "if": { + "properties": { + "serviceType": {"const": "scheduled"} + } + }, + "then": { + "required": ["appointmentTime"] + } +} +``` + +## Custom models + +Configure which AI model performs the extraction: + + +```typescript title="TypeScript" +const structuredOutput = await vapi.structuredOutputs.create({ + name: "Sentiment Analysis", + type: "ai", + schema: { + type: "object", + properties: { + sentiment: { + type: "string", + enum: ["positive", "negative", "neutral"] + }, + confidence: { + type: "number", + minimum: 0, + maximum: 1 + } + } + }, + model: { + provider: "openai", + model: "gpt-4-turbo-preview", + temperature: 0.1, + messages: [ + { + role: "system", + content: "You are an expert at analyzing customer sentiment. Be precise and consistent." + }, + { + role: "user", + content: "Analyze the sentiment of this conversation:\n{{transcript}}" + } + ] + } +}); +``` + +```python title="Python" +structured_output = vapi.structured_outputs.create( + name="Sentiment Analysis", + type="ai", + schema={ + "type": "object", + "properties": { + "sentiment": { + "type": "string", + "enum": ["positive", "negative", "neutral"] + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + } + }, + model={ + "provider": "openai", + "model": "gpt-4-turbo-preview", + "temperature": 0.1, + "messages": [ + { + "role": "system", + "content": "You are an expert at analyzing customer sentiment. Be precise and consistent." + }, + { + "role": "user", + "content": "Analyze the sentiment of this conversation:\n{{transcript}}" + } + ] + } +) +``` + + +### Available variables + +Use these variables in custom prompts: + +- `{{transcript}}` - Full conversation transcript +- `{{messages}}` - Conversation messages array +- `{{callEndedReason}}` - How the call ended +- `{{structuredOutput.name}}` - Output name +- `{{structuredOutput.description}}` - Output description +- `{{structuredOutput.schema}}` - Schema definition + +## API reference + + + +### Create structured output + + + Display name for the structured output (max 40 characters) + + + + Must be set to "ai" + + + + Description of what data to extract + + + + JSON Schema defining the structure of data to extract + + + + Array of assistant IDs to link this output to + + + + Custom model configuration for extraction + + +### Update structured output + + + + +To update the top level schema type after creation, you must include `?schemaOverride=true` as a query parameter in the URL + + +### List structured outputs + + + +Query parameters: +- `page` - Page number (default: 1) +- `limit` - Results per page (default: 20, max: 100) + +### Delete structured output + + + +## Common use cases + +### Customer information collection + +```json +{ + "name": "Customer Profile", + "type": "ai", + "schema": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "email": {"type": "string", "format": "email"}, + "phone": {"type": "string"}, + "accountNumber": {"type": "string"}, + "preferredContactMethod": { + "type": "string", + "enum": ["email", "phone", "sms"] + } + } + } +} +``` + +### Appointment scheduling + +```json +{ + "name": "Appointment Request", + "type": "ai", + "schema": { + "type": "object", + "properties": { + "preferredDate": {"type": "string", "format": "date"}, + "preferredTime": {"type": "string", "format": "time"}, + "duration": {"type": "integer", "enum": [15, 30, 45, 60]}, + "serviceType": { + "type": "string", + "enum": ["consultation", "follow-up", "procedure"] + }, + "notes": {"type": "string"} + }, + "required": ["preferredDate", "preferredTime", "serviceType"] + } +} +``` + +### Order processing + +```json +{ + "name": "Order Details", + "type": "ai", + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "product": {"type": "string"}, + "quantity": {"type": "integer", "minimum": 1}, + "specialInstructions": {"type": "string"} + }, + "required": ["product", "quantity"] + } + }, + "deliveryAddress": { + "type": "object", + "properties": { + "street": {"type": "string"}, + "city": {"type": "string"}, + "zipCode": {"type": "string", "pattern": "^\\d{5}$"} + } + }, + "deliveryInstructions": {"type": "string"} + } + } +} +``` + +### Lead qualification + +```json +{ + "name": "Lead Information", + "type": "ai", + "schema": { + "type": "object", + "properties": { + "company": {"type": "string"}, + "role": {"type": "string"}, + "budget": { + "type": "string", + "enum": ["< $10k", "$10k-50k", "$50k-100k", "> $100k"] + }, + "timeline": { + "type": "string", + "enum": ["immediate", "1-3 months", "3-6 months", "6+ months"] + }, + "painPoints": { + "type": "array", + "items": {"type": "string"} + }, + "nextSteps": {"type": "string"} + } + } +} +``` + +## Best practices + + + + Begin with basic schemas and add complexity as needed. Test with real conversations before adding advanced features. + + + + Help the AI understand what to extract by using clear field names and descriptions in your schema. + + + + Balance flexibility with validation. Too strict and extraction may fail; too loose and data quality suffers. + + + + Only mark fields as required if they're truly essential. Use optional fields for information that might not be mentioned. + + + +### Performance tips + +- **Keep schemas focused**: Extract only what you need to minimize processing time +- **Use appropriate models**: GPT-4 for complex schemas, GPT-3.5 for simple ones +- **Set low temperature**: Use 0.1 or lower for consistent extraction +- **Monitor success rates**: Track extraction failures and adjust schemas accordingly + +### Error handling + +Always check for null results which indicate extraction failure: + +```typescript +if (data.result === null) { + console.log(`Extraction failed for ${data.name}`); + // Implement fallback logic +} +``` + +## Troubleshooting + +### No data extracted + + + + Ensure your JSON Schema is valid and properly formatted + + + Confirm the required information was actually mentioned + + + Verify the structured output ID is linked to your assistant + + + Try a basic schema to isolate the issue + + + +### Incorrect extraction + +- Add more descriptive field descriptions +- Provide examples in custom prompts +- Use stricter validation patterns +- Lower the model temperature + +### Partial extraction + +- Make fields optional if they might not be mentioned +- Check if schema complexity exceeds limits +- Verify data types match expected values + +## Limitations + + +- Schema updates require `?schemaOverride=true` parameter +- Extraction occurs after call completion (not real-time) +- Name field limited to 40 characters + + +## Related + +- [Call analysis](/assistants/call-analysis) - Summarize and evaluate calls +- [Custom tools](/tools/custom-tools) - Trigger actions during calls +- [Webhooks](/server-url) - Receive extracted data via webhooks +- [Variables](/assistants/dynamic-variables) - Use dynamic data in conversations \ No newline at end of file diff --git a/fern/docs.yml b/fern/docs.yml index 9692fae99..d1ae8ee03 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -213,6 +213,16 @@ navigation: - page: Trieve integration path: knowledge-base/integrating-with-trieve.mdx icon: fa-light fa-brain + - section: Structured outputs + icon: fa-light fa-database + path: assistants/structured-outputs.mdx + contents: + - page: Quickstart + path: assistants/structured-outputs-quickstart.mdx + icon: fa-light fa-rocket + - page: Examples + path: assistants/structured-outputs-examples.mdx + icon: fa-light fa-code - page: Custom keywords path: customization/custom-keywords.mdx icon: fa-light fa-bullseye From 595931df0551b907e262bc049e52a58a0c552ff5 Mon Sep 17 00:00:00 2001 From: roshan Date: Mon, 25 Aug 2025 11:31:00 -0500 Subject: [PATCH 2/3] chore: Added Section for HIPPA Enabled Orgs --- fern/assistants/structured-outputs-quickstart.mdx | 8 +++++--- fern/assistants/structured-outputs.mdx | 14 +++++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/fern/assistants/structured-outputs-quickstart.mdx b/fern/assistants/structured-outputs-quickstart.mdx index 9dcb85570..02f5f6a2e 100644 --- a/fern/assistants/structured-outputs-quickstart.mdx +++ b/fern/assistants/structured-outputs-quickstart.mdx @@ -638,14 +638,16 @@ You can attach multiple structured outputs to extract different types of data: { artifactPlan: { structuredOutputIds: [ - customerInfoId, // Extract customer details - appointmentId, // Extract appointment requests - feedbackId // Extract satisfaction feedback + "550e8400-e29b-41d4-a716-446655440001", // Customer details extraction + "550e8400-e29b-41d4-a716-446655440002", // Appointment requests extraction + "550e8400-e29b-41d4-a716-446655440003" // Satisfaction feedback extraction ] } } ``` +The `structuredOutputIds` are UUIDs returned when you create each structured output configuration. + ### Conditional extraction Use conditional logic in your schema to handle different scenarios: diff --git a/fern/assistants/structured-outputs.mdx b/fern/assistants/structured-outputs.mdx index b474f1199..b7a53ea8e 100644 --- a/fern/assistants/structured-outputs.mdx +++ b/fern/assistants/structured-outputs.mdx @@ -756,9 +756,21 @@ if (data.result === null) { ### Partial extraction - Make fields optional if they might not be mentioned -- Check if schema complexity exceeds limits - Verify data types match expected values +## HIPAA compliance + + +**Important for HIPAA-enabled organizations:** + +If your organization has HIPAA compliance enabled (`hipaaEnabled: true`), structured outputs are **disabled by default** to protect PHI (Protected Health Information). + +To use structured outputs with HIPAA compliance: +- Contact the Vapi team to enable structured outputs +- Ensure you understand the implications for PHI handling +- Follow all HIPAA compliance best practices when extracting sensitive health data + + ## Limitations From 78a0f5fc3b5bd2483d6ba9aa03359f82948b4d86 Mon Sep 17 00:00:00 2001 From: roshan Date: Mon, 25 Aug 2025 15:53:01 -0500 Subject: [PATCH 3/3] removed integration examples. added additional examples of output --- .../structured-outputs-examples.mdx | 647 ++++++++---------- .../structured-outputs-quickstart.mdx | 93 ++- 2 files changed, 368 insertions(+), 372 deletions(-) diff --git a/fern/assistants/structured-outputs-examples.mdx b/fern/assistants/structured-outputs-examples.mdx index d78bf1b86..26560fc24 100644 --- a/fern/assistants/structured-outputs-examples.mdx +++ b/fern/assistants/structured-outputs-examples.mdx @@ -105,40 +105,25 @@ Extract patient information and appointment preferences from healthcare calls. } ``` -```javascript title="Integration Example" -// Process extracted healthcare appointment data -function processHealthcareAppointment(extractedData) { - const { patient, appointment } = extractedData; - - // Create patient record if new - if (appointment.type === 'new-patient') { - createPatientRecord(patient); - } - - // Check insurance eligibility - if (patient.insuranceProvider && patient.memberID) { - verifyInsurance(patient.insuranceProvider, patient.memberID); - } - - // Find available slots - const availableSlots = findAppointmentSlots({ - department: appointment.department, - dates: appointment.preferredDates, - timeSlot: appointment.preferredTimeSlot, - duration: appointment.type === 'new-patient' ? 60 : 30 - }); - - // Handle urgent cases - if (appointment.urgency === 'urgent') { - prioritizeAppointment(patient, appointment); - notifyOnCallStaff(appointment.department, appointment.symptoms); - } - - return { - patientId: patient.id, - appointmentOptions: availableSlots, - requiresPreAuth: checkPreAuthorization(appointment.type, patient.insuranceProvider) - }; +```json title="Example Output" +{ + "patient": { + "firstName": "Sarah", + "lastName": "Johnson", + "dateOfBirth": "1985-03-15", + "phoneNumber": "+14155551234", + "insuranceProvider": "Blue Cross Blue Shield", + "memberID": "BCB123456789" + }, + "appointment": { + "type": "follow-up", + "department": "cardiology", + "preferredDates": ["2024-01-15", "2024-01-16", "2024-01-17"], + "preferredTimeSlot": "morning", + "symptoms": ["chest pain", "shortness of breath"], + "urgency": "urgent" + }, + "additionalNotes": "Patient prefers female doctor if available" } ``` @@ -310,61 +295,52 @@ Capture order details, shipping information, and payment preferences. } ``` -```python title="Integration Example" -def process_ecommerce_order(extracted_data): - """Process extracted e-commerce order data""" - - # Validate inventory - for item in extracted_data['items']: - stock = check_inventory( - product=item['productName'], - quantity=item['quantity'], - size=item.get('size'), - color=item.get('color') - ) - - if not stock['available']: - suggest_alternatives(item, stock['alternatives']) - - # Calculate pricing - subtotal = calculate_subtotal(extracted_data['items']) - shipping_cost = calculate_shipping( - method=extracted_data['shipping']['method'], - address=extracted_data['shipping']['address'], - items=extracted_data['items'] - ) - - # Apply promotions - discount = 0 - if promo_code := extracted_data.get('promotions', {}).get('promoCode'): - discount = apply_promo_code(promo_code, subtotal) - - # Apply loyalty benefits - if loyalty_num := extracted_data['customer'].get('loyaltyNumber'): - loyalty_discount = calculate_loyalty_discount(loyalty_num, subtotal) - discount += loyalty_discount - - # Create order - order = { - 'customer': extracted_data['customer'], - 'items': extracted_data['items'], - 'shipping': extracted_data['shipping'], - 'payment': extracted_data['payment'], - 'subtotal': subtotal, - 'shipping_cost': shipping_cost, - 'discount': discount, - 'total': subtotal + shipping_cost - discount, - 'status': 'pending_payment' +```json title="Example Output" +{ + "customer": { + "name": "Michael Chen", + "email": "michael.chen@example.com", + "phone": "+14155552468", + "loyaltyNumber": "LOYAL123456" + }, + "items": [ + { + "productName": "Wireless Headphones XL", + "quantity": 1, + "size": null, + "color": "Black", + "price": 149.99 + }, + { + "productName": "USB-C Cable 6ft", + "quantity": 2, + "size": null, + "color": null, + "price": 19.99 } - - # Handle gift orders - if extracted_data['shipping'].get('giftWrap'): - order['gift_options'] = { - 'wrap': True, - 'message': extracted_data['shipping'].get('giftMessage') - } - - return create_order_record(order) + ], + "shipping": { + "address": { + "street": "123 Market Street", + "city": "San Francisco", + "state": "CA", + "zip": "94102", + "country": "USA" + }, + "method": "express", + "priority": "standard", + "giftWrap": false + }, + "payment": { + "method": "credit-card", + "lastFourDigits": "4242" + }, + "promotions": { + "promoCode": "SAVE20", + "giftWrap": false + }, + "specialInstructions": "Please leave package with doorman" +} ``` @@ -560,68 +536,56 @@ Qualify real estate leads and capture property preferences. } ``` -```javascript title="Integration Example" -// CRM integration for real estate leads -async function processRealEstateLead(extractedData) { - const { contact, propertySearch, timeline, currentSituation, leadScore } = extractedData; - - // Score the lead - const score = calculateLeadScore({ - budget: propertySearch.budget, - timeline: timeline.urgency, - preApproved: propertySearch.budget?.preApproved, - motivation: leadScore?.motivation, - currentlyOwns: currentSituation?.currentlyOwns - }); - - // Search matching properties - const matchingProperties = await searchProperties({ - type: propertySearch.propertyType, - locations: propertySearch.locations.map(l => l.area), - priceRange: { - min: propertySearch.budget.min, - max: propertySearch.budget.max +```json title="Example Output" +{ + "contact": { + "name": "Jennifer Martinez", + "email": "jmartinez@example.com", + "phone": "+14085551234", + "preferredContactMethod": "email" + }, + "propertySearch": { + "propertyType": "single-family", + "locations": [ + { + "area": "Palo Alto", + "importance": "high" + }, + { + "area": "Mountain View", + "importance": "medium" + } + ], + "budget": { + "min": 1500000, + "max": 2200000, + "preApproved": true }, - features: { - minBedrooms: propertySearch.features?.bedrooms, - minBathrooms: propertySearch.features?.bathrooms, - mustHaves: propertySearch.features?.mustHaves + "features": { + "bedrooms": 4, + "bathrooms": 3, + "squareFeet": 2500, + "mustHaves": ["garage", "backyard", "good schools"], + "niceToHaves": ["pool", "home office"] } - }); - - // Create lead in CRM - const leadId = await createCRMLead({ - contact, - score, - propertyPreferences: propertySearch, - timeline, - assignedAgent: selectBestAgent(propertySearch.locations, propertySearch.type) - }); - - // Set up automated follow-up - if (timeline.urgency === 'immediate' || leadScore?.followUpPriority === 'hot') { - scheduleImmediateCallback(leadId, contact); - sendPropertyMatches(contact.email, matchingProperties.slice(0, 5)); - } else { - scheduleDripCampaign(leadId, timeline.urgency); - } - - // Handle special situations - if (currentSituation?.firstTimeBuyer) { - sendFirstTimeBuyerGuide(contact.email); + }, + "timeline": { + "urgency": "3-6-months", + "moveInDate": "2024-06-01", + "reasonForMove": "job relocation" + }, + "currentSituation": { + "currentlyOwns": false, + "renting": true, + "firstTimeBuyer": false, + "needToSell": false + }, + "leadScore": { + "motivation": "high", + "financialReadiness": "qualified", + "decisionTimeframe": "actively-looking", + "followUpPriority": "warm" } - - if (currentSituation?.needToSell) { - scheduleListingConsultation(leadId); - } - - return { - leadId, - score, - matchingProperties: matchingProperties.length, - assignedAgent: leadId.agent, - nextAction: determineNextAction(score, timeline) - }; } ``` @@ -821,85 +785,56 @@ Capture insurance claim details and incident information. } ``` -```python title="Integration Example" -def process_insurance_claim(extracted_data): - """Process insurance claim intake""" - - # Verify policy - policy = verify_policy( - policy_number=extracted_data['policyholder']['policyNumber'], - name=extracted_data['policyholder']['name'], - dob=extracted_data['policyholder'].get('dateOfBirth') - ) - - if not policy['active']: - return { - 'status': 'rejected', - 'reason': 'Policy not active', - 'action': 'transfer_to_agent' - } - - # Check coverage - coverage = check_coverage( - policy_type=policy['type'], - incident_type=extracted_data['incident']['type'], - incident_date=extracted_data['incident']['date'] - ) - - # Create claim - claim = { - 'claimNumber': generate_claim_number(), - 'policyholder': extracted_data['policyholder'], - 'incident': extracted_data['incident'], - 'damages': extracted_data.get('damages', {}), - 'status': 'intake_complete', - 'coveredAmount': coverage['estimated_coverage'], - 'deductible': coverage['deductible'] - } - - # Handle injuries - if injuries := extracted_data.get('damages', {}).get('injuries'): - for injury in injuries: - if injury.get('medicalTreatment'): - claim['requires_medical_review'] = True - schedule_medical_review(claim['claimNumber'], injury) - - # Handle emergency situations - if extracted_data.get('urgency') == 'emergency': - claim['priority'] = 'high' - dispatch_emergency_adjuster(claim) - - # Arrange temporary solutions - if extracted_data['incident']['type'] == 'property': - arrange_emergency_repairs(extracted_data['incident']['location']) - elif extracted_data['incident']['type'] == 'auto': - arrange_rental_car(extracted_data['policyholder']) - - # Documentation requirements - required_docs = determine_required_documentation( - incident_type=extracted_data['incident']['type'], - damages=extracted_data.get('damages'), - amount=extracted_data.get('damages', {}).get('estimatedTotalLoss', 0) - ) - - # Create claim record - claim_id = save_claim(claim) - - # Send confirmation - send_claim_confirmation( - email=extracted_data['policyholder']['email'], - claim_number=claim['claimNumber'], - next_steps=required_docs, - adjuster_contact=claim.get('assigned_adjuster') - ) - - return { - 'claimNumber': claim['claimNumber'], - 'status': 'accepted', - 'estimatedProcessingTime': estimate_processing_time(claim), - 'requiredDocuments': required_docs, - 'nextSteps': generate_next_steps(claim) +```json title="Example Output" +{ + "policyholder": { + "name": "Robert Thompson", + "policyNumber": "POL-2024-789456", + "dateOfBirth": "1975-08-22", + "email": "rthompson@example.com", + "phone": "+15105551234" + }, + "incident": { + "type": "auto", + "date": "2024-01-10", + "time": "14:30", + "location": { + "street": "Highway 101 North", + "city": "San Jose", + "state": "CA", + "zip": "95110" + }, + "description": "Rear-ended at traffic light, other driver at fault", + "policeReportNumber": "SJ-2024-001234", + "otherPartyInvolved": true + }, + "damages": { + "vehicleDamage": { + "description": "Rear bumper and trunk damage", + "driveable": true, + "airbagDeployed": false + }, + "injuries": [ + { + "person": "policyholder", + "type": "whiplash", + "medicalTreatment": true, + "hospital": "Valley Medical Center" + } + ], + "estimatedTotalLoss": 8500, + "propertyDamage": null + }, + "witnesses": [ + { + "name": "Maria Garcia", + "phone": "+14085559876", + "statement": "Saw the other car hit from behind at red light" } + ], + "urgency": "standard", + "additionalInfo": "Other driver admitted fault, have dashcam footage available" +} ``` @@ -1117,94 +1052,72 @@ Process loan or credit applications with financial information. } ``` -```javascript title="Integration Example" -async function processFinancialApplication(extractedData) { - const { applicant, loanDetails, coApplicant } = extractedData; - - // Initial eligibility check - const eligibility = await checkEligibility({ - creditScore: applicant.financial?.creditScore, - income: applicant.employment.annualIncome, - employmentStatus: applicant.employment.status, - loanAmount: loanDetails.amount, - loanType: loanDetails.type - }); - - if (!eligibility.qualified) { - return { - status: 'declined', - reason: eligibility.reason, - alternatives: suggestAlternatives(eligibility, loanDetails) - }; - } - - // Calculate debt-to-income ratio - const monthlyIncome = (applicant.employment.annualIncome + - (applicant.employment.otherIncome || 0)) / 12; - const dti = (applicant.financial?.monthlyDebt || 0) / monthlyIncome; - - // Risk assessment - const riskScore = calculateRiskScore({ - creditScore: applicant.financial?.creditScore, - dti, - employmentYears: applicant.employment.yearsEmployed, - bankruptcyHistory: applicant.financial?.bankruptcyHistory, - collateral: loanDetails.collateral, - existingCustomer: applicant.financial?.existingAccounts?.length > 0 - }); - - // Determine loan terms - const terms = generateLoanTerms({ - loanType: loanDetails.type, - amount: loanDetails.amount, - term: loanDetails.term, - riskScore, - preferredRate: extractedData.preferences?.preferredRate, - hasCollateral: loanDetails.collateral?.type !== 'none' - }); - - // Create application - const applicationId = await createApplication({ - applicant, - loanDetails, - coApplicant: coApplicant?.hasCoApplicant ? coApplicant : null, - eligibility, - riskScore, - proposedTerms: terms, - status: determineInitialStatus(riskScore, loanDetails.amount) - }); - - // Handle co-applicant - if (coApplicant?.hasCoApplicant) { - await requestCoApplicantInfo(applicationId, coApplicant); - } - - // Schedule next steps - if (riskScore.requiresManualReview) { - await assignToUnderwriter(applicationId, riskScore.reviewReasons); - } else if (riskScore.preApproved) { - await sendPreApprovalLetter(applicant.personalInfo.email, terms); - } - - // Set up document collection - const requiredDocs = determineRequiredDocuments({ - loanType: loanDetails.type, - amount: loanDetails.amount, - employmentStatus: applicant.employment.status, - incomeVerifiable: applicant.employment.incomeVerifiable - }); - - await sendDocumentRequest(applicant.personalInfo.email, requiredDocs); - - return { - applicationId, - status: terms.status, - preApproved: riskScore.preApproved, - estimatedRate: terms.rate, - monthlyPayment: terms.monthlyPayment, - requiredDocuments: requiredDocs, - nextSteps: generateApplicationNextSteps(riskScore, requiredDocs) - }; +```json title="Example Output" +{ + "applicant": { + "personalInfo": { + "firstName": "David", + "lastName": "Kim", + "dateOfBirth": "1988-11-15", + "ssn": "***-**-6789", + "email": "dkim@example.com", + "phone": "+12065551234", + "currentAddress": { + "street": "456 Pine Street", + "city": "Seattle", + "state": "WA", + "zip": "98101", + "yearsAtAddress": 3 + } + }, + "employment": { + "status": "full-time", + "employerName": "Tech Corp", + "jobTitle": "Senior Engineer", + "yearsEmployed": 5, + "annualIncome": 150000, + "otherIncome": 12000, + "incomeVerifiable": true + }, + "financial": { + "creditScore": 750, + "monthlyDebt": 2500, + "bankruptcyHistory": false, + "existingAccounts": ["checking", "savings", "credit-card"], + "accountNumbers": ["****1234", "****5678"] + } + }, + "loanDetails": { + "type": "mortgage", + "amount": 450000, + "term": 30, + "purpose": "home-purchase", + "propertyAddress": { + "street": "789 Oak Avenue", + "city": "Bellevue", + "state": "WA", + "zip": "98004" + }, + "propertyValue": 550000, + "downPayment": 100000, + "collateral": { + "type": "real-estate", + "value": 550000, + "description": "Single family home" + } + }, + "coApplicant": { + "hasCoApplicant": true, + "relationship": "spouse", + "name": "Sarah Kim", + "income": 85000 + }, + "preferences": { + "preferredRate": "fixed", + "preferredPaymentDate": 1, + "autopayInterest": true + }, + "additionalInfo": "Looking to close within 45 days, have pre-approval from another lender" } ``` @@ -1251,71 +1164,65 @@ Track these metrics for production deployments: - Schema validation failures - Most commonly missing fields -## Integration patterns +## Output data structure -### Webhook processing +### Webhook payload format -```javascript -// Robust webhook handler with error handling -app.post('/vapi/structured-output', async (req, res) => { - try { - const { type, call } = req.body; - - if (type !== 'call.ended') { - return res.status(200).send('OK'); - } - - const outputs = call.artifact?.structuredOutputs || {}; - - for (const [outputId, data] of Object.entries(outputs)) { - if (!data.result) { - console.error(`Extraction failed for ${data.name}`); - await notifyExtractionFailure(call.id, data.name); - continue; - } - - try { - await processExtractedData(data.name, data.result); - } catch (error) { - console.error(`Processing failed for ${data.name}:`, error); - await queueForRetry(call.id, outputId, data); +When structured outputs are extracted, they're delivered in this format: + +```json +{ + "type": "call.ended", + "call": { + "id": "call_abc123", + "artifact": { + "structuredOutputs": { + "550e8400-e29b-41d4-a716-446655440001": { + "name": "Customer Support Ticket", + "result": { + "customer": { + "name": "John Smith", + "email": "john@example.com" + }, + "issue": { + "description": "Login issues", + "priority": "high" + } + } + }, + "550e8400-e29b-41d4-a716-446655440002": { + "name": "Satisfaction Score", + "result": { + "score": 8, + "feedback": "Very helpful agent" + } + } } } - - res.status(200).send('OK'); - } catch (error) { - console.error('Webhook processing error:', error); - res.status(500).send('Internal error'); } -}); +} ``` -### Batch processing +### API response format + +When retrieving call data via API: -```python -def batch_process_extractions(call_ids): - """Process multiple calls in batch""" - - results = [] - - for call_id in call_ids: - call = vapi.calls.get(call_id) - outputs = call.artifact.get('structuredOutputs', {}) - - for output_id, data in outputs.items(): - if data['result']: - results.append({ - 'call_id': call_id, - 'output_name': data['name'], - 'data': data['result'], - 'extracted_at': call.ended_at - }) - - # Bulk insert to database - if results: - bulk_insert_extracted_data(results) - - return len(results) +```json +{ + "id": "call_abc123", + "status": "ended", + "endedAt": "2024-01-10T15:30:00Z", + "artifact": { + "structuredOutputs": { + "outputId1": { + "name": "Output Name", + "result": { + // Your extracted data here + } + } + } + } +} ``` ## Related resources diff --git a/fern/assistants/structured-outputs-quickstart.mdx b/fern/assistants/structured-outputs-quickstart.mdx index 02f5f6a2e..30ae147d1 100644 --- a/fern/assistants/structured-outputs-quickstart.mdx +++ b/fern/assistants/structured-outputs-quickstart.mdx @@ -8,6 +8,29 @@ slug: assistants/structured-outputs-quickstart This quickstart guide will help you set up structured outputs to automatically extract customer information from phone calls. In just a few minutes, you'll create a structured output, link it to an assistant, and test data extraction. +### What are structured outputs? + +Structured outputs are AI-powered data extraction templates that automatically capture and organize information from conversations. They work by: + +1. **Listening to conversations** - As your assistant talks with customers, structured outputs analyze the conversation in real-time +2. **Extracting key information** - Based on your defined schema, they identify and extract relevant data points like names, emails, preferences, and issues +3. **Validating and formatting** - The extracted data is validated against your schema rules and formatted into clean, structured JSON +4. **Delivering results** - The structured data is available immediately after the call ends via API or webhooks + +### When are structured outputs generated? + +Structured outputs are processed: +- **During the call** - Data is extracted in real-time as the conversation happens +- **After call completion** - Final validation and formatting occurs when the call ends +- **Available via** - Call artifacts in the API response or webhook events + +### Why use structured outputs? + +- **Automate data entry** - No more manual transcription or form filling +- **Ensure consistency** - Every call captures the same structured information +- **Enable integrations** - Automatically sync data to CRMs, ticketing systems, or databases +- **Improve analytics** - Structured data is easier to analyze and report on + ## What you'll build A customer support assistant that automatically extracts: @@ -28,7 +51,71 @@ A customer support assistant that automatically extracts: ## Step 1: Create your structured output -First, define what information you want to extract using a JSON Schema: +You can create structured outputs using either the Dashboard UI or the API. + +### Option A: Using the Dashboard (Recommended for beginners) + + + + 1. Log in to [dashboard.vapi.ai](https://dashboard.vapi.ai) + 2. Click on **Structured Outputs** in the left sidebar + 3. Click **Create New Structured Output** + + + + 1. **Name**: Enter "Support Ticket" + 2. **Type**: Select "AI" (for automatic extraction) + 3. **Description**: Add "Extract support ticket information from customer calls" + + + + Use the visual schema builder or paste this JSON directly: + ```json + { + "type": "object", + "properties": { + "customer": { + "type": "object", + "properties": { + "name": {"type": "string", "description": "Customer full name"}, + "email": {"type": "string", "format": "email", "description": "Customer email"}, + "phone": {"type": "string", "description": "Customer phone number"} + }, + "required": ["name"] + }, + "issue": { + "type": "object", + "properties": { + "description": {"type": "string", "description": "Issue description"}, + "category": { + "type": "string", + "enum": ["billing", "technical", "general", "complaint"], + "description": "Issue category" + }, + "priority": { + "type": "string", + "enum": ["low", "medium", "high", "urgent"], + "description": "Priority level" + } + }, + "required": ["description", "category"] + } + }, + "required": ["customer", "issue"] + } + ``` + + + + 1. Click **Create Structured Output** + 2. Copy the generated ID from the details page + 3. You'll use this ID to link to your assistant + + + +### Option B: Using the API + +Define what information you want to extract using a [JSON Schema](https://json-schema.org/learn/getting-started-step-by-step). JSON Schema is a standard for describing data structures - [learn more about JSON Schema here](https://json-schema.org/understanding-json-schema/). ```bash title="cURL" @@ -604,7 +691,9 @@ curl -X PATCH "https://api.vapi.ai/assistant/YOUR_ASSISTANT_ID" \ -H "Authorization: Bearer $VAPI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ - "serverUrl": "https://your-domain.com/vapi/webhook" + "server": { + "url": "https://your-domain.com/vapi/webhook" + } }' ```