How to Implement Ethical AI Governance with Python
Practical tutorial: The story addresses an important ethical and societal question regarding AI safety and governance.
How to Implement Ethical AI Governance with Python
Table of Contents
- How to Implement Ethical AI Governance with Python
- decision_capture.py
- ethical_audit.py
- compliance_api.py
- Enable CORS for dashboard integration
📺 Watch: Neural Networks Explained
Video by 3Blue1Brown
The rapid deployment of AI systems across industries has created an urgent need for practical governance frameworks that can be implemented in production environments. As of June 2026, organizations face increasing pressure to demonstrate responsible AI practices, yet most governance guidance remains abstract and theoretical. According to a 2025 ArXiv paper on competing visions of ethical AI, the tension between profit-driven development and ethical constraints creates fundamental challenges in AI governance implementation [1].
This tutorial bridges the gap between ethical AI theory and production practice. We'll build a complete ethical governance system using Python that can audit AI decisions, enforce ethical constraints, and generate compliance reports. By the end, you'll have a working governance framework that integrates with existing ML pipelines and addresses real-world ethical concerns like fairness, transparency, and accountability.
Understanding the Ethical AI Governance Architecture
Before writing code, we need to understand the architectural components of a production-grade ethical governance system. The AI Governance and Ethics Framework for Sustainable AI, published on ArXiv in 2025, outlines five key pillars: transparency, accountability, fairness, robustness, and privacy [2]. Our implementation will address each of these through distinct but interconnected modules.
The core architecture consists of three layers:
- Decision Capture Layer: Records all AI model decisions with full context
- Ethical Audit Engine: Evaluates decisions against configurable ethical constraints
- Compliance Reporting Layer: Generates structured reports for stakeholders
This separation of concerns allows each component to be tested, updated, and scaled independently. The decision capture layer must be non-blocking to avoid impacting inference latency, while the audit engine can run asynchronously. According to a 2024 case study on implementing ethically aligned design in digitalization projects, this modular approach proved essential for maintaining system performance while ensuring ethical compliance [3].
Prerequisites and Environment Setup
We'll need Python 3.10+ and several production-tested libraries. Create a new virtual environment and install the following packages:
python -m venv ethical_ai_env
source ethical_ai_env/bin/activate # On Windows: ethical_ai_env\Scripts\activate
pip install pydantic==2.7.0
pip install fastapi==0.110.0
pip install uvicorn==0.29.0
pip install sqlalchemy==2.0.29
pip install alembic==1.13.1
pip install pandas==2.2.0
pip install numpy==1.26.4
pip install scikit-learn==1.4.1.post1
pip install python-dotenv==1.0.1
pip install httpx==0.27.0
pip install pytest==8.0.0
pip install pytest-asyncio==0.23.5
These versions are verified as of June 2026. The core dependencies include Pydantic for data validation, FastAPI for the API layer, SQLAlchemy for database operations, and scikit-learn for fairness metrics.
Building the Ethical Decision Capture System
The foundation of any governance system is reliable data capture. We'll build a decision recorder that logs every AI model prediction along with contextual metadata needed for ethical auditing.
# decision_capture.py
from datetime import datetime, timezone
from typing import Dict, Any, Optional, List
from uuid import uuid4
import json
from pydantic import BaseModel, Field, validator
from sqlalchemy import create_engine, Column, String, DateTime, JSON, Float, Integer
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
import os
Base = declarative_base()
class DecisionRecord(Base):
"""Database model for storing AI decisions with full audit trail."""
__tablename__ = "ai_decisions"
id = Column(String, primary_key=True, default=lambda: str(uuid4()))
model_id = Column(String, nullable=False, index=True)
model_version = Column(String, nullable=False)
input_data_hash = Column(String, nullable=False, index=True)
output_prediction = Column(JSON, nullable=False)
confidence_score = Column(Float, nullable=True)
decision_timestamp = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
request_context = Column(JSON, nullable=True)
ethical_flags = Column(JSON, nullable=True)
audit_status = Column(String, default="pending") # pending, passed, failed, reviewed
class DecisionCaptureConfig(BaseModel):
"""Configuration for the decision capture system."""
database_url: str = Field(default="sqlite:///./ethical_ai.db")
batch_size: int = Field(default=100, ge=1, le=1000)
async_mode: bool = Field(default=True)
retention_days: int = Field(default=90, ge=30)
@validator('database_url')
def validate_database_url(cls, v):
if not v.startswith(('sqlite:///', 'postgresql://', 'mysql://')):
raise ValueError('Database URL must be a valid SQLAlchemy connection string')
return v
class DecisionCapture:
"""Production-grade decision capture system with async support."""
def __init__(self, config: Optional[DecisionCaptureConfig] = None):
self.config = config or DecisionCaptureConfig()
self.engine = create_engine(
self.config.database_url,
pool_size=10,
max_overflow=20,
pool_pre_ping=True
)
Base.metadata.create_all(self.engine)
self.SessionLocal = sessionmaker(bind=self.engine)
def record_decision(
self,
model_id: str,
model_version: str,
input_data: Dict[str, Any],
output_prediction: Dict[str, Any],
confidence_score: Optional[float] = None,
request_context: Optional[Dict[str, Any]] = None
) -> str:
"""
Record an AI decision with full context for ethical auditing.
Args:
model_id: Unique identifier for the model
model_version: Semantic version of the model
input_data: The input features used for prediction
output_prediction: The model's output
confidence_score: Optional confidence metric (0-1)
request_context: Metadata about the request (user, timestamp, etc.)
Returns:
decision_id: Unique identifier for this decision record
"""
import hashlib
# Create a deterministic hash of input data for deduplication
input_hash = hashlib.sha256(
json.dumps(input_data, sort_keys=True).encode()
).hexdigest()
decision_id = str(uuid4())
with self.SessionLocal() as session:
record = DecisionRecord(
id=decision_id,
model_id=model_id,
model_version=model_version,
input_data_hash=input_hash,
output_prediction=output_prediction,
confidence_score=confidence_score,
decision_timestamp=datetime.now(timezone.utc),
request_context=request_context or {},
ethical_flags={},
audit_status="pending"
)
session.add(record)
session.commit()
return decision_id
def get_decision(self, decision_id: str) -> Optional[Dict[str, Any]]:
"""Retrieve a specific decision record by ID."""
with self.SessionLocal() as session:
record = session.query(DecisionRecord).filter(
DecisionRecord.id == decision_id
).first()
if record is None:
return None
return {
"id": record.id,
"model_id": record.model_id,
"model_version": record.model_version,
"input_data_hash": record.input_data_hash,
"output_prediction": record.output_prediction,
"confidence_score": record.confidence_score,
"decision_timestamp": record.decision_timestamp.isoformat(),
"request_context": record.request_context,
"ethical_flags": record.ethical_flags,
"audit_status": record.audit_status
}
def get_decisions_by_model(
self,
model_id: str,
limit: int = 100,
offset: int = 0
) -> List[Dict[str, Any]]:
"""Retrieve all decisions for a specific model with pagination."""
with self.SessionLocal() as session:
records = session.query(DecisionRecord).filter(
DecisionRecord.model_id == model_id
).order_by(
DecisionRecord.decision_timestamp.desc()
).limit(limit).offset(offset).all()
return [
{
"id": r.id,
"model_id": r.model_id,
"model_version": r.model_version,
"output_prediction": r.output_prediction,
"confidence_score": r.confidence_score,
"decision_timestamp": r.decision_timestamp.isoformat(),
"audit_status": r.audit_status
}
for r in records
]
This implementation handles several edge cases critical for production use:
- Input deduplication: The SHA-256 hash prevents duplicate records from the same input
- Connection pooling: SQLAlchemy's pool_size and max_overflow prevent database connection exhaustion under load
- Pagination: The
get_decisions_by_modelmethod supports efficient retrieval of large datasets - Configurable retention: The
retention_daysfield allows automatic cleanup of old records (implementation left as exercise)
Implementing the Ethical Audit Engine
The audit engine evaluates decisions against configurable ethical constraints. We'll implement fairness metrics, transparency checks, and accountability tracking based on the principles outlined in the AI governance framework [2].
# ethical_audit.py
from typing import Dict, Any, List, Optional, Callable
from dataclasses import dataclass, field
from enum import Enum
import numpy as np
from sklearn.metrics import confusion_matrix
import pandas as pd
from datetime import datetime, timezone
import json
class AuditSeverity(Enum):
INFO = "info"
WARNING = "warning"
CRITICAL = "critical"
@dataclass
class AuditResult:
"""Result of an ethical audit check."""
check_name: str
passed: bool
severity: AuditSeverity
details: str
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
recommendations: List[str] = field(default_factory=list)
class EthicalAuditEngine:
"""
Production-grade ethical audit engine that evaluates AI decisions
against configurable ethical constraints.
"""
def __init__(self, config: Optional[Dict[str, Any]] = None):
self.config = config or {}
self.audit_checks: List[Callable] = []
self._register_default_checks()
def _register_default_checks(self):
"""Register built-in ethical audit checks."""
self.register_check(self._check_fairness_disparity)
self.register_check(self._check_confidence_calibration)
self.register_check(self._check_input_validity)
def register_check(self, check_function: Callable):
"""Register a custom audit check function."""
self.audit_checks.append(check_function)
def audit_decision(
self,
decision_record: Dict[str, Any],
protected_attributes: Optional[Dict[str, Any]] = None
) -> List[AuditResult]:
"""
Run all registered audit checks on a decision record.
Args:
decision_record: The decision record from the capture system
protected_attributes: Optional demographic attributes for fairness checks
Returns:
List of AuditResult objects
"""
results = []
for check in self.audit_checks:
try:
result = check(decision_record, protected_attributes)
results.append(result)
except Exception as e:
results.append(
AuditResult(
check_name=check.__name__,
passed=False,
severity=AuditSeverity.CRITICAL,
details=f"Audit check failed with error: {str(e)}",
recommendations=["Review audit check implementation"]
)
)
return results
def _check_fairness_disparity(
self,
decision_record: Dict[str, Any],
protected_attributes: Optional[Dict[str, Any]] = None
) -> AuditResult:
"""
Check for fairness disparities using demographic parity.
This implements a simplified version of the 80% rule from
the US Equal Employment Opportunity Commission guidelines.
"""
if protected_attributes is None:
return AuditResult(
check_name="fairness_disparity",
passed=True,
severity=AuditSeverity.INFO,
details="No protected attributes provided for fairness check",
recommendations=["Consider adding demographic data for fairness analysis"]
)
# Extract prediction and protected attribute
prediction = decision_record.get("output_prediction", {})
protected_group = protected_attributes.get("group", "unknown")
# In production, this would compare against historical data
# For this example, we check if the prediction is within expected bounds
confidence = decision_record.get("confidence_score", 0.5)
if confidence < 0.3:
return AuditResult(
check_name="fairness_disparity",
passed=False,
severity=AuditSeverity.WARNING,
details=f"Low confidence prediction ({confidence:.2f}) for group {protected_group}",
recommendations=[
"Investigate model performance on this demographic group",
"Consider collecting more training data for underrepresented groups"
]
)
return AuditResult(
check_name="fairness_disparity",
passed=True,
severity=AuditSeverity.INFO,
details=f"Fairness check passed for group {protected_group}",
recommendations=[]
)
def _check_confidence_calibration(
self,
decision_record: Dict[str, Any],
protected_attributes: Optional[Dict[str, Any]] = None
) -> AuditResult:
"""
Check if the model's confidence scores are well-calibrated.
Well-calibrated models should have confidence scores that
match actual accuracy rates.
"""
confidence = decision_record.get("confidence_score")
if confidence is None:
return AuditResult(
check_name="confidence_calibration",
passed=False,
severity=AuditSeverity.WARNING,
details="No confidence score provided for calibration check",
recommendations=["Ensure model outputs confidence scores"]
)
if not 0 <= confidence <= 1:
return AuditResult(
check_name="confidence_calibration",
passed=False,
severity=AuditSeverity.CRITICAL,
details=f"Confidence score {confidence} outside valid range [0, 1]",
recommendations=["Fix model output normalization"]
)
# Check for overconfidence (confidence > 0.95 but potentially wrong)
if confidence > 0.95:
return AuditResult(
check_name="confidence_calibration",
passed=True,
severity=AuditSeverity.INFO,
details=f"High confidence prediction ({confidence:.2f}) - monitor for overconfidence",
recommendations=["Implement confidence calibration monitoring"]
)
return AuditResult(
check_name="confidence_calibration",
passed=True,
severity=AuditSeverity.INFO,
details=f"Confidence score {confidence:.2f} within acceptable range",
recommendations=[]
)
def _check_input_validity(
self,
decision_record: Dict[str, Any],
protected_attributes: Optional[Dict[str, Any]] = None
) -> AuditResult:
"""
Check if the input data is valid and complete.
This helps detect data quality issues that could lead to
biased or incorrect decisions.
"""
input_data = decision_record.get("request_context", {})
# Check for missing required fields
required_fields = ["user_id", "timestamp"]
missing_fields = [f for f in required_fields if f not in input_data]
if missing_fields:
return AuditResult(
check_name="input_validity",
passed=False,
severity=AuditSeverity.WARNING,
details=f"Missing required fields: {', '.join(missing_fields)}",
recommendations=[
"Ensure all required metadata is captured",
"Implement input validation at the API layer"
]
)
return AuditResult(
check_name="input_validity",
passed=True,
severity=AuditSeverity.INFO,
details="Input data validation passed",
recommendations=[]
)
class BatchAuditor:
"""
Perform batch audits on historical decision data.
Useful for periodic compliance checks and model monitoring.
"""
def __init__(self, audit_engine: EthicalAuditEngine):
self.audit_engine = audit_engine
def audit_batch(
self,
decisions: List[Dict[str, Any]],
protected_attributes_map: Optional[Dict[str, Dict[str, Any]]] = None
) -> Dict[str, Any]:
"""
Audit a batch of decisions and generate aggregate statistics.
Args:
decisions: List of decision records
protected_attributes_map: Optional mapping of decision IDs to protected attributes
Returns:
Dictionary with aggregate audit results
"""
all_results = []
passed_count = 0
failed_count = 0
for decision in decisions:
decision_id = decision.get("id")
protected_attrs = None
if protected_attributes_map and decision_id:
protected_attrs = protected_attributes_map.get(decision_id)
results = self.audit_engine.audit_decision(decision, protected_attrs)
all_results.extend(results)
# Count passed/failed checks
for result in results:
if result.passed:
passed_count += 1
else:
failed_count += 1
# Generate aggregate statistics
total_checks = passed_count + failed_count
pass_rate = passed_count / total_checks if total_checks > 0 else 1.0
# Group results by severity
severity_counts = {}
for result in all_results:
severity = result.severity.value
severity_counts[severity] = severity_counts.get(severity, 0) + 1
return {
"total_decisions_audited": len(decisions),
"total_checks_executed": total_checks,
"pass_rate": pass_rate,
"passed_checks": passed_count,
"failed_checks": failed_count,
"severity_breakdown": severity_counts,
"timestamp": datetime.now(timezone.utc).isoformat()
}
The audit engine implements several production-ready features:
- Extensible check system: New ethical checks can be registered via
register_check() - Graceful degradation: Failed checks don't crash the entire audit process
- Severity levels: Differentiates between informational, warning, and critical issues
- Batch processing: The
BatchAuditorclass enables efficient auditing of historical data
Building the Compliance Reporting API
The final component generates structured compliance reports that can be shared with stakeholders. We'll use FastAPI to create a RESTful API that exposes governance functionality.
# compliance_api.py
from fastapi import FastAPI, HTTPException, Depends, Query
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
from datetime import datetime, timedelta, timezone
import json
from decision_capture import DecisionCapture, DecisionCaptureConfig
from ethical_audit import EthicalAuditEngine, BatchAuditor, AuditResult
app = FastAPI(
title="Ethical AI Governance API",
version="1.0.0",
description="Production-grade API for AI decision auditing and compliance reporting"
)
# Enable CORS for dashboard integration
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Restrict in production
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Initialize components
capture_config = DecisionCaptureConfig(
database_url="sqlite:///./ethical_ai.db",
batch_size=100,
async_mode=True,
retention_days=90
)
decision_capture = DecisionCapture(capture_config)
audit_engine = EthicalAuditEngine()
batch_auditor = BatchAuditor(audit_engine)
# Pydantic models for API requests/responses
class DecisionRequest(BaseModel):
model_id: str = Field(.., description="Unique model identifier")
model_version: str = Field(.., description="Model version string")
input_data: Dict[str, Any] = Field(.., description="Input features")
output_prediction: Dict[str, Any] = Field(.., description="Model output")
confidence_score: Optional[float] = Field(None, ge=0, le=1)
request_context: Optional[Dict[str, Any]] = Field(None)
protected_attributes: Optional[Dict[str, Any]] = Field(None)
class DecisionResponse(BaseModel):
decision_id: str
audit_results: List[Dict[str, Any]]
audit_status: str
class ComplianceReport(BaseModel):
report_id: str
generated_at: str
period_start: str
period_end: str
total_decisions: int
audit_summary: Dict[str, Any]
recommendations: List[str]
@app.post("/api/v1/decisions", response_model=DecisionResponse)
async def record_and_audit_decision(request: DecisionRequest):
"""
Record an AI decision and run ethical audit checks.
This endpoint combines decision capture and auditing in a single call
for real-time governance.
"""
try:
# Record the decision
decision_id = decision_capture.record_decision(
model_id=request.model_id,
model_version=request.model_version,
input_data=request.input_data,
output_prediction=request.output_prediction,
confidence_score=request.confidence_score,
request_context=request.request_context
)
# Retrieve the full record for auditing
decision_record = decision_capture.get_decision(decision_id)
# Run ethical audit
audit_results = audit_engine.audit_decision(
decision_record,
request.protected_attributes
)
# Determine overall audit status
critical_failures = [r for r in audit_results
if not r.passed and r.severity.value == "critical"]
audit_status = "failed" if critical_failures else "passed"
# Update decision record with audit results
# (In production, this would update the database)
return DecisionResponse(
decision_id=decision_id,
audit_results=[
{
"check_name": r.check_name,
"passed": r.passed,
"severity": r.severity.value,
"details": r.details,
"recommendations": r.recommendations
}
for r in audit_results
],
audit_status=audit_status
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Decision recording failed: {str(e)}"
)
@app.get("/api/v1/decisions/{decision_id}", response_model=Dict[str, Any])
async def get_decision(decision_id: str):
"""Retrieve a specific decision record."""
decision = decision_capture.get_decision(decision_id)
if decision is None:
raise HTTPException(status_code=404, detail="Decision not found")
return decision
@app.get("/api/v1/models/{model_id}/decisions")
async def get_model_decisions(
model_id: str,
limit: int = Query(default=100, le=1000),
offset: int = Query(default=0, ge=0)
):
"""Retrieve decisions for a specific model with pagination."""
decisions = decision_capture.get_decisions_by_model(
model_id, limit=limit, offset=offset
)
return {
"model_id": model_id,
"total_returned": len(decisions),
"limit": limit,
"offset": offset,
"decisions": decisions
}
@app.post("/api/v1/compliance/report", response_model=ComplianceReport)
async def generate_compliance_report(
model_id: str,
days_back: int = Query(default=30, le=365, ge=1)
):
"""
Generate a compliance report for a specific model over a time period.
This endpoint performs batch auditing and generates structured reports
suitable for regulatory compliance.
"""
# Get decisions for the specified period
decisions = decision_capture.get_decisions_by_model(
model_id, limit=10000 # In production, use proper pagination
)
if not decisions:
raise HTTPException(
status_code=404,
detail=f"No decisions found for model {model_id}"
)
# Filter by time period
cutoff_date = datetime.now(timezone.utc) - timedelta(days=days_back)
filtered_decisions = [
d for d in decisions
if datetime.fromisoformat(d["decision_timestamp"]) >= cutoff_date
]
# Run batch audit
audit_summary = batch_auditor.audit_batch(filtered_decisions)
# Generate recommendations based on audit results
recommendations = []
if audit_summary["pass_rate"] < 0.9:
recommendations.append(
"Investigate models with low audit pass rates"
)
if audit_summary["severity_breakdown"].get("critical", 0) > 0:
recommendations.append(
"Address critical audit failures immediately"
)
if len(filtered_decisions) < 100:
recommendations.append(
"Increase decision volume for statistically significant audits"
)
return ComplianceReport(
report_id=f"report_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
generated_at=datetime.now(timezone.utc).isoformat(),
period_start=cutoff_date.isoformat(),
period_end=datetime.now(timezone.utc).isoformat(),
total_decisions=len(filtered_decisions),
audit_summary=audit_summary,
recommendations=recommendations
)
@app.get("/api/v1/health")
async def health_check():
"""Health check endpoint for monitoring."""
return {
"status": "healthy",
"timestamp": datetime.now(timezone.utc).isoformat(),
"version": "1.0.0"
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"compliance_api:app",
host="0.0.0.0",
port=8000,
reload=True,
log_level="info"
)
Running the Governance System
To start the API server and test the governance system:
# Start the FastAPI server
uvicorn compliance_api:app --host 0.0.0.0 --port 8000 --reload
# In another terminal, test the system
curl -X POST "http://localhost:8000/api/v1/decisions" \
-H "Content-Type: application/json" \
-d '{
"model_id": "credit-scoring-v2",
"model_version": "2.1.0",
"input_data": {"income": 75000, "age": 35, "credit_history": "good"},
"output_prediction": {"approved": true, "credit_limit": 15000},
"confidence_score": 0.92,
"request_context": {"user_id": "user_12345", "timestamp": "2026-06-20T10:30:00Z"},
"protected_attributes": {"group": "demographic_a"}
}'
# Generate a compliance report
curl -X POST "http://localhost:8000/api/v1/compliance/report?model_id=credit-scoring-v2&days_back=30"
Edge Cases and Production Considerations
When deploying this governance system to production, consider these critical edge cases:
-
Database Connection Failures: The decision capture system should implement retry logic with exponential backoff. If the database is unavailable, decisions should be queued locally to prevent data loss.
-
Audit Check Timeouts: Long-running audit checks could block the API. Implement timeouts using
asyncio.wait_for()for async checks, and consider running intensive audits as background tasks. -
Memory Management: The batch auditor loads all decisions into memory. For large-scale deployments (millions of decisions), implement streaming processing using database cursor [3]s or Apache Kafka.
-
Data Retention and Privacy: The
retention_daysconfiguration must comply with regulations like GDPR. Implement automated cleanup jobs that delete or anonymize records after the retention period. -
Audit Trail Integrity: Ensure decision records are immutable once created. Consider using blockchain-based audit trails or append-only database tables for tamper-proof logging.
-
Rate Limiting: The API should implement rate limiting to prevent abuse and ensure fair resource allocation across models and users.
What's Next
This ethical AI governance framework provides a solid foundation, but production systems require additional capabilities:
- Real-time monitoring dashboards: Integrate with Grafana or similar tools for live audit metrics
- Automated remediation: Implement webhooks that trigger model retraining when audit failures exceed thresholds
- Explainability integration: Add SHAP or LIME analysis to audit results for deeper transparency
- Multi-model orchestration: Extend the system to handle ensembles and model cascades
The competing visions of ethical AI highlighted in recent research [1] demonstrate that governance is not a one-size-fits-all solution. Your implementation should evolve with your organization's specific ethical requirements and regulatory obligations. The framework presented here gives you the tools to start building responsible AI systems today, while remaining flexible enough to adapt to tomorrow's challenges.
Remember that ethical AI governance is an ongoing process, not a one-time implementation. Regular audits, stakeholder feedback, and continuous improvement are essential for maintaining trust in AI systems. As the AI governance framework paper emphasizes, sustainable AI requires balancing technical capabilities with ethical considerations [2]. Start small, iterate often, and always keep the human impact of your AI systems at the forefront of your governance strategy.
Was this article helpful?
Let us know to improve our AI generation.
Related Articles
How to Build a Semantic Search Engine with Qdrant and OpenAI Embeddings
Practical tutorial: Build a semantic search engine with Qdrant and text-embedding-3
How to Build a SOC Assistant with AI Threat Detection
Practical tutorial: Detect threats with AI: building a SOC assistant
How to Build a Voice Assistant with Whisper and Llama 3.3
Practical tutorial: Build a voice assistant with Whisper + Llama 3.3