AI Wealth Advisory Avatar
Build a personalized AI financial advisor with an animated avatar.
Introduction
30 Second Summary
When you walk into a bank and ask a financial advisor for investment guidance, they review your income, spending habits, and goals before making recommendations. Today, that same personalized experience is being delivered at scale through AI-powered digital advisors.
In this project, you will build an AI Wealth Advisory Avatar that analyzes customer financial profiles and delivers personalized investment recommendations through an animated chat interface.
What You'll Build
A Python FastAPI backend that processes mock customer financial data and uses Claude Haiku to generate tailored wealth advisory responses, paired with a web-based animated avatar frontend.
By the end of this project, you'll have:
- A FastAPI backend with advisory endpoints that process customer financial profiles and generate AI-powered recommendations.
- A system prompt engineered to produce compliant, data-driven financial guidance that references specific numbers from each customer's profile.
- An animated avatar interface with chat-style interaction that delivers advice conversationally in a mobile-app-style layout.
- Secret Mission: Add a portfolio risk scoring engine that proactively warns customers about concentration risk.
Are there any prerequisites?
You should have basic Python knowledge and familiarity with HTML, CSS, and JavaScript. All tools and accounts are included in the setup step.
You will need to purchase $5 in credits on the Anthropic API platform. This covers hundreds of advisory conversations during development and testing.
Set Up the Project Environment
Before you can build the AI advisory engine, you need a working Python environment with access to the Anthropic API.
In this step, you will create a project directory, install the required Python packages, and connect to the Anthropic API to confirm everything works end to end.
In this step, get ready to:
- Create a project directory with a Python virtual environment and install dependencies.
- Set up an Anthropic API account with credits and generate an API key.
- Verify the setup by calling the Anthropic API and confirming a response.
Create the project directory and install dependencies
Every Python project starts with a dedicated folder and an isolated virtual environment. The virtual environment keeps this project's packages separate from your system Python.
- Check your Python version by running this command:
python3 --version
✔️ I see version 3.10 or higher
Your Python version is compatible. You are ready to continue.
ⓧ I see a version lower than 3.10
This project requires Python 3.10 or higher. On macOS, the system Python is often outdated.
- Install the latest Python from python.org/downloads.
- After installing, run python3 --version again to confirm it shows 3.10 or higher.
- If the old version still appears, use the full path (e.g. python3.12 --version) or install a version manager like pyenv.
ⓧ Command not found
Python is not installed on your system yet.
- Download and install Python from python.org/downloads.
- On Windows, check the box labeled Add Python to PATH during installation.
- After installing, close and reopen your terminal, then run python3 --version to confirm.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir wealth-advisor
cd wealth-advisor
- Create a Python virtual environment by running:
python3 -m venv venv
- Activate the virtual environment:
macOS
source venv/bin/activate
Windows
venv\Scripts\activate
You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
Make sure you ran the activate command from inside the wealth-advisor folder. If the venv folder does not exist, re-run python3 -m venv venv first.
On Windows, if you see an execution policy error, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned and try activating again.
Help me troubleshoot
- Create the requirements.txt file that lists all project dependencies. In your code editor, create a new file called requirements.txt inside the wealth-advisor folder and paste in the following:
fastapi[standard]==0.138.1
anthropic==0.112.0
uvicorn==0.34.0
python-dotenv==1.1.0
What are these packages?
- fastapi[standard] is the web framework for building your advisory API endpoints.
- anthropic is the official Python SDK for calling the Claude AI models.
- uvicorn is the server that runs your FastAPI application.
- python-dotenv loads environment variables from your .env file so you can keep secrets out of your code.
- Install all the dependencies by running:
pip install -r requirements.txt
You should see a series of download and install messages ending with a success confirmation.
Installation failed?
If you see a permissions error, confirm your (venv) prefix is visible. Installing without an active virtual environment can require admin permissions.
If a package fails to download, check your internet connection and try running the command again.
Help me troubleshoot
- Confirm the files exist by running:
ls
You should see requirements.txt and venv in the output.
Create an Anthropic API account and configure your key
The advisory engine uses Claude Haiku 4.5 to generate personalized financial guidance. To access the model, you need an Anthropic API key.
- Open your browser and go to console.anthropic.com.
- Click Continue with Google or enter your email and click Continue with email to create your account.
- Complete any verification steps that appear (email confirmation, organization name).
- Once you are in the dashboard, navigate to the billing section and purchase $5 in credits. This is the minimum for Tier 1 API access.
- Navigate to Settings then API Keys.
- Click Create Key.
- Give the key a name (e.g. wealth-advisor) and click Create Key to confirm.
- Copy your new API key immediately. It is only shown once.
Why $5 in credits?
Anthropic requires a $5 credit purchase to unlock Tier 1 API access. Claude Haiku 4.5 costs $1 per million input tokens and $5 per million output tokens, so $5 will support hundreds of advisory conversations during development.
- Back in your code editor, create a new file called .env inside the wealth-advisor folder. Paste in the following, replacing the placeholder with your actual key:
ANTHROPIC_API_KEY=your-api-key-here
Why use a .env file?
Storing your API key in a .env file keeps it out of your source code. The python-dotenv package loads it into your application at runtime without hardcoding secrets.
- Save the .env file.
Can't see the .env file in your editor?
Files starting with a dot are hidden by default on macOS and Linux. In VS Code, they appear in the Explorer sidebar automatically. If you are using Finder, press Cmd+Shift+. to reveal hidden files.
Help me find my .env file
Verify the setup with a test API call
Before building the full application, confirm that your API key works and the Anthropic SDK can reach the Claude model.
- Create a file called test_setup.py in your wealth-advisor folder and paste in the following:
import os
from anthropic import Anthropic
from dotenv import load_dotenv
# Load the API key from the .env file
load_dotenv()
# Create the Anthropic client
client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
# Send a test message to Claude Haiku
message = client.messages.create(
model="claude-haiku-4-5-20250609",
max_tokens=100,
messages=[{"role": "user", "content": "Say hello in one sentence."}],
)
# Print the response to confirm the connection works
print("API connection successful!")
print("Response:", message.content[0].text)
What does this script do?
- load_dotenv() reads your .env file and makes ANTHROPIC_API_KEY available as an environment variable.
- Anthropic(api_key=...) creates a client that can talk to the Claude API.
- client.messages.create(...) sends a simple prompt to Claude Haiku 4.5 and returns the response.
- If you see a response printed, your API key and SDK are both working correctly.
- Save test_setup.py and run it:
python3 test_setup.py
You should see output like this:
API connection successful!
Response: Hello! How can I help you today?
What should I see?
The exact wording of the response will vary each time since Claude generates unique text. The important thing is that you see "API connection successful!" followed by any short greeting.
Seeing an authentication error?
Double-check that your .env file is in the wealth-advisor folder (the same folder you are running the script from). Make sure there are no extra spaces around the = sign or quotes around the key value.
If you see a "credit balance" or "rate limit" error, confirm that your $5 credit purchase completed successfully on the Anthropic dashboard.
Help me troubleshoot
- Now that the test passes, you can delete the test file. It is not needed for the rest of the project:
rm test_setup.py
✔️ Awesome, I've got everything!
Great. Make sure you have saved requirements.txt and .env in your wealth-advisor folder.
ⓧ I'd like to double check the full code
Here are the two files you should have at this point:
ANTHROPIC_API_KEY=your-api-key-here
fastapi[standard]==0.138.1
anthropic==0.112.0
uvicorn==0.34.0
python-dotenv==1.1.0
Your environment is ready and your API connection is confirmed. Next up, you will build the customer financial data model that powers personalized advisory responses.
Build the Customer Financial Data Model
Your environment is wired up and the Anthropic API is responding. Now it's time to give your AI advisor something to work with: real customer financial data.
A financial advisor needs to know a client's spending habits, investment portfolio, and risk tolerance before giving personalized guidance. In this step, you'll create mock customer profiles that mirror what a real bank's data system would provide, then build a service layer to load and analyze them.
In this step, get ready to:
- Create mock customer profiles with realistic financial data.
- Define data models that validate and structure financial information.
- Build a data service that loads profiles and computes portfolio metrics.
Create mock customer financial profiles
Your AI advisor needs customer data to personalize its responses. You'll create three mock profiles that represent different life stages and financial situations.
Each profile includes monthly spending breakdowns, investment portfolios, risk tolerance scores, and financial goals.
- Create a data/ directory inside your project folder by running this command:
mkdir data
- Confirm the folder exists by running:
ls
You should see data listed alongside your other project files.
- Create a new file called customers.json inside the data/ folder and paste the following content:
[
{
"id": "cust_001",
"name": "Sarah Chen",
"age": 29,
"income": 95000,
"risk_tolerance": 8,
"financial_goals": ["retirement", "home_purchase"],
"spending": [
{"category": "housing", "monthly_amount": 2200},
{"category": "food", "monthly_amount": 600},
{"category": "transportation", "monthly_amount": 350},
{"category": "entertainment", "monthly_amount": 400},
{"category": "savings", "monthly_amount": 1500},
{"category": "other", "monthly_amount": 850}
],
"portfolio": [
{"asset_class": "equities", "allocation_percent": 65, "value": 45000},
{"asset_class": "bonds", "allocation_percent": 15, "value": 10350},
{"asset_class": "crypto", "allocation_percent": 12, "value": 8280},
{"asset_class": "cash", "allocation_percent": 8, "value": 5520}
]
},
{
"id": "cust_002",
"name": "Michael Torres",
"age": 52,
"income": 180000,
"risk_tolerance": 4,
"financial_goals": ["retirement", "wealth_preservation"],
"spending": [
{"category": "housing", "monthly_amount": 3500},
{"category": "food", "monthly_amount": 1200},
{"category": "transportation", "monthly_amount": 800},
{"category": "entertainment", "monthly_amount": 600},
{"category": "savings", "monthly_amount": 4000},
{"category": "other", "monthly_amount": 1900}
],
"portfolio": [
{"asset_class": "equities", "allocation_percent": 40, "value": 320000},
{"asset_class": "bonds", "allocation_percent": 35, "value": 280000},
{"asset_class": "real_estate", "allocation_percent": 15, "value": 120000},
{"asset_class": "cash", "allocation_percent": 10, "value": 80000}
]
},
{
"id": "cust_003",
"name": "Priya Sharma",
"age": 38,
"income": 130000,
"risk_tolerance": 6,
"financial_goals": ["children_education", "retirement", "investment_growth"],
"spending": [
{"category": "housing", "monthly_amount": 2800},
{"category": "food", "monthly_amount": 900},
{"category": "transportation", "monthly_amount": 500},
{"category": "education", "monthly_amount": 1200},
{"category": "savings", "monthly_amount": 2500},
{"category": "other", "monthly_amount": 1100}
],
"portfolio": [
{"asset_class": "equities", "allocation_percent": 50, "value": 150000},
{"asset_class": "bonds", "allocation_percent": 25, "value": 75000},
{"asset_class": "mutual_funds", "allocation_percent": 15, "value": 45000},
{"asset_class": "cash", "allocation_percent": 10, "value": 30000}
]
}
]
What does this data represent?
Each customer profile represents a different investor archetype.
- Sarah Chen (age 29): A young professional with high risk tolerance (8/10), aggressive equity allocation, and growth-focused goals.
- Michael Torres (age 52): A pre-retiree with low risk tolerance (4/10), a large diversified portfolio, and preservation-focused goals.
- Priya Sharma (age 38): A mid-career parent balancing education funding, retirement, and growth across multiple asset classes.
- Save data/customers.json.
- Verify the file is valid JSON by running:
python -c "import json; data = json.load(open('data/customers.json')); print(len(data), 'customer profiles loaded')"
You should see 3 customer profiles loaded printed in your terminal.
Seeing a JSON decode error?
- Check that you copied the entire file, including the opening [ and closing ] brackets.
- Make sure there are no trailing commas after the last item in any array or object.
- Confirm the file is saved inside the data/ folder, not the project root.
Help me fix my customers.json file
✔️ Awesome, I've got everything!
Great. Make sure data/customers.json is saved before moving on.
ⓧ I'd like to double check the full code
Here is the complete data/customers.json file:
[
{
"id": "cust_001",
"name": "Sarah Chen",
"age": 29,
"income": 95000,
"risk_tolerance": 8,
"financial_goals": ["retirement", "home_purchase"],
"spending": [
{"category": "housing", "monthly_amount": 2200},
{"category": "food", "monthly_amount": 600},
{"category": "transportation", "monthly_amount": 350},
{"category": "entertainment", "monthly_amount": 400},
{"category": "savings", "monthly_amount": 1500},
{"category": "other", "monthly_amount": 850}
],
"portfolio": [
{"asset_class": "equities", "allocation_percent": 65, "value": 45000},
{"asset_class": "bonds", "allocation_percent": 15, "value": 10350},
{"asset_class": "crypto", "allocation_percent": 12, "value": 8280},
{"asset_class": "cash", "allocation_percent": 8, "value": 5520}
]
},
{
"id": "cust_002",
"name": "Michael Torres",
"age": 52,
"income": 180000,
"risk_tolerance": 4,
"financial_goals": ["retirement", "wealth_preservation"],
"spending": [
{"category": "housing", "monthly_amount": 3500},
{"category": "food", "monthly_amount": 1200},
{"category": "transportation", "monthly_amount": 800},
{"category": "entertainment", "monthly_amount": 600},
{"category": "savings", "monthly_amount": 4000},
{"category": "other", "monthly_amount": 1900}
],
"portfolio": [
{"asset_class": "equities", "allocation_percent": 40, "value": 320000},
{"asset_class": "bonds", "allocation_percent": 35, "value": 280000},
{"asset_class": "real_estate", "allocation_percent": 15, "value": 120000},
{"asset_class": "cash", "allocation_percent": 10, "value": 80000}
]
},
{
"id": "cust_003",
"name": "Priya Sharma",
"age": 38,
"income": 130000,
"risk_tolerance": 6,
"financial_goals": ["children_education", "retirement", "investment_growth"],
"spending": [
{"category": "housing", "monthly_amount": 2800},
{"category": "food", "monthly_amount": 900},
{"category": "transportation", "monthly_amount": 500},
{"category": "education", "monthly_amount": 1200},
{"category": "savings", "monthly_amount": 2500},
{"category": "other", "monthly_amount": 1100}
],
"portfolio": [
{"asset_class": "equities", "allocation_percent": 50, "value": 150000},
{"asset_class": "bonds", "allocation_percent": 25, "value": 75000},
{"asset_class": "mutual_funds", "allocation_percent": 15, "value": 45000},
{"asset_class": "cash", "allocation_percent": 10, "value": 30000}
]
}
]
Define Pydantic data models
Raw JSON is useful for storage, but your Python code needs structured, validated objects to work with. Pydantic models define the exact shape of your data and automatically reject anything that doesn't match.
- Create a new file called models.py in your project root and paste the following code:
from pydantic import BaseModel
# Represents one spending category in a customer's monthly budget
class SpendingCategory(BaseModel):
category: str
monthly_amount: float
# Represents one asset class in a customer's investment portfolio
class PortfolioAllocation(BaseModel):
asset_class: str
allocation_percent: float
value: float
# Full customer profile combining personal info, spending, and investments
class CustomerProfile(BaseModel):
id: str
name: str
age: int
income: float
risk_tolerance: int
financial_goals: list[str]
spending: list[SpendingCategory]
portfolio: list[PortfolioAllocation]
# The request body shape for the advisory endpoint
class AdvisoryRequest(BaseModel):
customer_id: str
question: str
What do these models do?
- SpendingCategory and PortfolioAllocation define the building blocks. Each spending entry has a category and amount. Each portfolio entry has an asset class, percentage, and dollar value.
- CustomerProfile brings everything together into one validated object. Pydantic will reject data that's missing fields or has wrong types.
- AdvisoryRequest defines what the frontend sends when asking for advice: a customer ID and a question.
- Save models.py.
- Verify the models load correctly by running:
python -c "from models import CustomerProfile; print('Models imported successfully')"
You should see Models imported successfully in your terminal.
Seeing an import error?
- Make sure models.py is in your project root (same level as requirements.txt), not inside the data/ folder.
- Check that your virtual environment is still activated. You should see (venv) at the start of your terminal prompt.
Help me fix my Pydantic model import error
✔️ Awesome, I've got everything!
Great. Make sure models.py is saved before moving on.
ⓧ I'd like to double check the full code
Here is the complete models.py file:
from pydantic import BaseModel
class SpendingCategory(BaseModel):
category: str
monthly_amount: float
class PortfolioAllocation(BaseModel):
asset_class: str
allocation_percent: float
value: float
class CustomerProfile(BaseModel):
id: str
name: str
age: int
income: float
risk_tolerance: int
financial_goals: list[str]
spending: list[SpendingCategory]
portfolio: list[PortfolioAllocation]
class AdvisoryRequest(BaseModel):
customer_id: str
question: str
Build the data service layer
With the data file and models in place, you need a service layer that loads customer profiles from disk and computes useful metrics from their financial data. This keeps data access logic separate from your API endpoints.
- Create a new file called data_service.py in your project root and paste the following code for loading customer data:
import json
from pathlib import Path
from models import CustomerProfile
# Path to the customer data file, relative to this script
DATA_PATH = Path(__file__).parent / "data" / "customers.json"
def load_customers() -> list[CustomerProfile]:
# Read the JSON file and convert each entry into a validated CustomerProfile
with open(DATA_PATH) as f:
raw = json.load(f)
return [CustomerProfile(**c) for c in raw]
def get_customer_by_id(customer_id: str) -> CustomerProfile | None:
# Search all customers and return the matching one, or None
customers = load_customers()
for customer in customers:
if customer.id == customer_id:
return customer
return None
What does this code do?
- DATA_PATH uses Path(__file__).parent to find the data/ folder relative to where this script lives. This works regardless of where you run the command from.
- load_customers() reads the JSON file and validates each entry through the CustomerProfile model. If any data is malformed, Pydantic raises an error immediately.
- get_customer_by_id() is a lookup helper that the API will use to fetch a specific customer's profile.
- Save data_service.py.
- You won't see a change yet. The next chunk adds the analytics function that completes this service.
- Now add the portfolio metrics function below get_customer_by_id() in data_service.py:
def calculate_portfolio_metrics(customer: CustomerProfile) -> dict:
# Calculate total value across all asset classes
total_value = sum(a.value for a in customer.portfolio)
num_asset_classes = len(customer.portfolio)
# Find the highest concentration in any single asset class
max_allocation = max(a.allocation_percent for a in customer.portfolio)
# Pull savings specifically from the spending breakdown
monthly_savings = sum(
s.monthly_amount for s in customer.spending if s.category == "savings"
)
monthly_spending = sum(s.monthly_amount for s in customer.spending)
return {
"total_portfolio_value": total_value,
"num_asset_classes": num_asset_classes,
"max_single_allocation_percent": max_allocation,
"monthly_savings": monthly_savings,
"monthly_spending": monthly_spending,
"savings_rate": round(monthly_savings / (customer.income / 12) * 100, 1),
}
What does this function calculate?
This function extracts key financial health indicators from a customer's profile. The AI advisor will reference these numbers when making recommendations.
- total_portfolio_value sums the dollar value across all asset classes.
- max_single_allocation_percent identifies concentration risk. If one asset class dominates, the advisor can flag it.
- savings_rate calculates what percentage of the customer's monthly income goes to savings.
- Save data_service.py.
- Test the full data service by running:
python -c "from data_service import load_customers, calculate_portfolio_metrics; customers = load_customers(); print(len(customers), 'customers loaded'); metrics = calculate_portfolio_metrics(customers[0]); print('Sarah portfolio:', metrics['total_portfolio_value'])"
You should see 3 customers loaded followed by Sarah portfolio: 69150 printed in your terminal.
Seeing a FileNotFoundError?
- Confirm that data/customers.json exists by running ls data/ in your terminal. You should see customers.json listed.
- Make sure you're running the command from your project root folder (where data_service.py lives).
- Check that your data_service.py file is saved in the project root, not inside the data/ folder.
Help me debug my data service
✔️ Awesome, I've got everything!
Great. Double check that data_service.py is saved with both the data loading functions and calculate_portfolio_metrics.
ⓧ I'd like to double check the full code
Here is the complete data_service.py file:
import json
from pathlib import Path
from models import CustomerProfile
DATA_PATH = Path(__file__).parent / "data" / "customers.json"
def load_customers() -> list[CustomerProfile]:
with open(DATA_PATH) as f:
raw = json.load(f)
return [CustomerProfile(**c) for c in raw]
def get_customer_by_id(customer_id: str) -> CustomerProfile | None:
customers = load_customers()
for customer in customers:
if customer.id == customer_id:
return customer
return None
def calculate_portfolio_metrics(customer: CustomerProfile) -> dict:
total_value = sum(a.value for a in customer.portfolio)
num_asset_classes = len(customer.portfolio)
max_allocation = max(a.allocation_percent for a in customer.portfolio)
monthly_savings = sum(
s.monthly_amount for s in customer.spending if s.category == "savings"
)
monthly_spending = sum(s.monthly_amount for s in customer.spending)
return {
"total_portfolio_value": total_value,
"num_asset_classes": num_asset_classes,
"max_single_allocation_percent": max_allocation,
"monthly_savings": monthly_savings,
"monthly_spending": monthly_spending,
"savings_rate": round(monthly_savings / (customer.income / 12) * 100, 1),
}
Your data layer is complete. You have three realistic customer profiles, validated data models, and a service that can load and analyze them. Next up, you'll build the AI advisory engine that uses this data to generate personalized financial guidance.
Build the AI Advisory Engine
Your customer data model is ready. Each profile captures spending patterns, portfolio allocations, and risk scores that paint a complete financial picture.
Now it's time to build the brain of the application. The advisory engine reads a customer's financial data, injects it into a carefully crafted prompt, and calls Claude to generate personalized investment guidance. This is the same architecture pattern used by firms like Morgan Stanley for their AI-assisted wealth advisors.
In this step, get ready to:
- Engineer a system prompt that produces compliant, data-driven financial advice.
- Build the function that calls Claude Haiku 4.5 with a customer's financial context.
- Create the FastAPI application with API endpoints for the frontend.
Engineer the system prompt
A generic AI response won't cut it for financial advice. The model needs to see the customer's actual numbers to produce recommendations that reference real dollar amounts, percentages, and timeframes.
You'll build a system prompt that defines the advisor persona and injects the customer's entire financial profile as context.
- Create a new file called advisory_engine.py in your project root (the same folder as models.py).
- Add the imports, API client setup, and system prompt by pasting this code:
import os
from anthropic import Anthropic
from dotenv import load_dotenv
from models import CustomerProfile
from data_service import calculate_portfolio_metrics
# Load the .env file so we can access the API key
load_dotenv()
# Initialize the Anthropic client with your API key
client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
# System prompt that defines the advisor persona and injects customer data
SYSTEM_PROMPT = """You are WealthAI, a professional digital wealth advisor integrated into a bank's mobile application. Your role is to provide personalized, data-driven financial guidance.
Guidelines:
- Always reference specific numbers from the customer's financial profile when making recommendations
- Provide actionable advice, not generic platitudes
- Use a warm but professional tone appropriate for a banking application
- Include specific percentages, dollar amounts, and timeframes in your suggestions
- Acknowledge the customer's risk tolerance level when suggesting investments
- Never guarantee returns or make promises about market performance
- Add a brief disclaimer at the end of investment-specific advice
- Keep responses concise (3-5 paragraphs maximum) for mobile readability
Customer Financial Profile:
Name: {name}
Age: {age}
Annual Income: ${income:,.0f}
Risk Tolerance: {risk_tolerance}/10
Financial Goals: {goals}
Monthly Spending Breakdown:
{spending_breakdown}
Investment Portfolio (Total: ${total_value:,.0f}):
{portfolio_breakdown}
Portfolio Metrics:
- Savings Rate: {savings_rate}% of income
- Number of Asset Classes: {num_assets}
- Highest Single Allocation: {max_allocation}%
"""
What does this code do?
The load_dotenv() call reads your .env file so the Anthropic client can authenticate with your API key.
The SYSTEM_PROMPT is a template string with placeholders like {name} and {spending_breakdown}. When a customer asks a question, these placeholders get filled with their real financial data. This is what makes the AI's advice personalized rather than generic.
The guidelines section instructs the model to always cite specific numbers, never guarantee returns, and keep responses short for mobile screens.
The system prompt is a static template. You need a function that takes a customer's profile and fills in all the placeholders with real data.
- Below the SYSTEM_PROMPT string, add the format_customer_context() function:
def format_customer_context(customer: CustomerProfile) -> str:
# Calculate metrics from the customer's portfolio
metrics = calculate_portfolio_metrics(customer)
# Format each spending category as a readable line
spending_lines = "\n".join(
f"- {s.category.replace('_', ' ').title()}: ${s.monthly_amount:,.0f}/month"
for s in customer.spending
)
# Format each asset class as a readable line
portfolio_lines = "\n".join(
f"- {a.asset_class.replace('_', ' ').title()}: {a.allocation_percent}% (${a.value:,.0f})"
for a in customer.portfolio
)
# Fill in all the placeholders in the system prompt
return SYSTEM_PROMPT.format(
name=customer.name,
age=customer.age,
income=customer.income,
risk_tolerance=customer.risk_tolerance,
goals=", ".join(g.replace("_", " ").title() for g in customer.financial_goals),
spending_breakdown=spending_lines,
total_value=metrics["total_portfolio_value"],
portfolio_breakdown=portfolio_lines,
savings_rate=metrics["savings_rate"],
num_assets=metrics["num_asset_classes"],
max_allocation=metrics["max_single_allocation_percent"],
)
What does this function do?
This function transforms raw customer data into a human-readable context string that the AI model can reason about.
- It calls calculate_portfolio_metrics() from your data service to get computed values like savings rate.
- It formats spending categories and portfolio allocations as readable bullet lists.
- It fills every placeholder in SYSTEM_PROMPT with actual customer numbers, so the AI sees real data.
- Save advisory_engine.py.
Seeing a syntax error?
The f-strings inside the list comprehensions contain curly braces and colons for formatting (like ${s.monthly_amount:,.0f}). Make sure you copied the code exactly, including the :,.0f format specifiers.
help me fix a syntax error in my format_customer_context function
Build the advice generation function
The format_customer_context() function prepares the context. Now you need the function that actually sends it to Claude and returns the AI's response.
- Below the format_customer_context() function in advisory_engine.py, add the generate_advice() function:
def generate_advice(customer: CustomerProfile, question: str) -> str:
# Build the full system prompt with this customer's data
system = format_customer_context(customer)
# Call Claude Haiku 4.5 with the customer context and question
message = client.messages.create(
model="claude-haiku-4-5-20250609",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": question}],
)
# Return the text content from Claude's response
return message.content[0].text
What does this function do?
This is the core function that connects your data to the AI model.
- It calls format_customer_context() to build the system prompt with real customer data.
- It sends that context plus the user's question to claude-haiku-4-5-20250609, which costs $1 per million input tokens. Your $5 credit supports hundreds of conversations.
- The system parameter sets the persona and data context. The messages parameter carries the user's actual question.
Why Claude Haiku 4.5 instead of a larger model?
Claude Haiku 4.5 costs $1 per million input tokens and $5 per million output tokens. Larger models like Sonnet cost $3/$15 per million tokens. Advisory responses don't need frontier reasoning capabilities. They need fast, clear answers that reference the data you provide.
Haiku's low latency also matters for a chat-style interface where users expect quick responses.
- Save advisory_engine.py.
You won't see a change yet. The next substep creates the API that exposes this function to the frontend.
Not sure if your API key is loaded?
Make sure your .env file is in the project root (the same directory as advisory_engine.py). The file should contain ANTHROPIC_API_KEY= followed by your actual key with no spaces around the equals sign.
help me troubleshoot my Anthropic API key loading in Python
✔️ Awesome, I've got everything!
Great. Double check you've saved advisory_engine.py and move on to the next substep.
ⓧ I'd like to double check the full code
import os
from anthropic import Anthropic
from dotenv import load_dotenv
from models import CustomerProfile
from data_service import calculate_portfolio_metrics
load_dotenv()
client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
SYSTEM_PROMPT = """You are WealthAI, a professional digital wealth advisor integrated into a bank's mobile application. Your role is to provide personalized, data-driven financial guidance.
Guidelines:
- Always reference specific numbers from the customer's financial profile when making recommendations
- Provide actionable advice, not generic platitudes
- Use a warm but professional tone appropriate for a banking application
- Include specific percentages, dollar amounts, and timeframes in your suggestions
- Acknowledge the customer's risk tolerance level when suggesting investments
- Never guarantee returns or make promises about market performance
- Add a brief disclaimer at the end of investment-specific advice
- Keep responses concise (3-5 paragraphs maximum) for mobile readability
Customer Financial Profile:
Name: {name}
Age: {age}
Annual Income: ${income:,.0f}
Risk Tolerance: {risk_tolerance}/10
Financial Goals: {goals}
Monthly Spending Breakdown:
{spending_breakdown}
Investment Portfolio (Total: ${total_value:,.0f}):
{portfolio_breakdown}
Portfolio Metrics:
- Savings Rate: {savings_rate}% of income
- Number of Asset Classes: {num_assets}
- Highest Single Allocation: {max_allocation}%
"""
def format_customer_context(customer: CustomerProfile) -> str:
metrics = calculate_portfolio_metrics(customer)
spending_lines = "\n".join(
f"- {s.category.replace('_', ' ').title()}: ${s.monthly_amount:,.0f}/month"
for s in customer.spending
)
portfolio_lines = "\n".join(
f"- {a.asset_class.replace('_', ' ').title()}: {a.allocation_percent}% (${a.value:,.0f})"
for a in customer.portfolio
)
return SYSTEM_PROMPT.format(
name=customer.name,
age=customer.age,
income=customer.income,
risk_tolerance=customer.risk_tolerance,
goals=", ".join(g.replace("_", " ").title() for g in customer.financial_goals),
spending_breakdown=spending_lines,
total_value=metrics["total_portfolio_value"],
portfolio_breakdown=portfolio_lines,
savings_rate=metrics["savings_rate"],
num_assets=metrics["num_asset_classes"],
max_allocation=metrics["max_single_allocation_percent"],
)
def generate_advice(customer: CustomerProfile, question: str) -> str:
system = format_customer_context(customer)
message = client.messages.create(
model="claude-haiku-4-5-20250609",
max_tokens=1024,
system=system,
messages=[{"role": "user", "content": question}],
)
return message.content[0].text
Create the FastAPI application
The advisory engine exists as a Python module, but there's no way for a frontend to talk to it yet. You need an API layer that accepts HTTP requests and routes them to your data service and advisory engine.
You'll use FastAPI to create a lightweight web server with endpoints the frontend can call.
- Create an empty static/ directory in your project root by running this command:
mkdir static
Why create an empty static folder now?
The FastAPI app will be configured to serve frontend files from this directory. If the folder doesn't exist when the server starts, it raises an error. You'll add the frontend files here in the next step.
- Create a new file called main.py in your project root.
- Add the app setup with CORS middleware by pasting this code:
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from models import AdvisoryRequest
from data_service import get_customer_by_id, load_customers, calculate_portfolio_metrics
from advisory_engine import generate_advice
# Create the FastAPI application
app = FastAPI(title="WealthAI Advisory API")
# Allow the frontend to make requests to the backend
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
What is CORS middleware?
CORS (Cross-Origin Resource Sharing) controls which websites can make requests to your API. Setting allow_origins=["*"] means any frontend can call your backend. This is fine for local development.
Now add the API endpoints that the frontend will call. These routes connect incoming requests to your data service and advisory engine.
- Below the CORS middleware block in main.py, add the endpoint functions and static file mount:
@app.get("/health")
def health_check():
return {"status": "healthy", "service": "WealthAI Advisory"}
@app.get("/customers")
def list_customers():
# Return a simplified list of customers for the dropdown
customers = load_customers()
return [
{"id": c.id, "name": c.name, "age": c.age, "income": c.income}
for c in customers
]
@app.get("/customers/{customer_id}")
def get_customer(customer_id: str):
customer = get_customer_by_id(customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
metrics = calculate_portfolio_metrics(customer)
return {"profile": customer.model_dump(), "metrics": metrics}
@app.post("/advice")
def get_advice(request: AdvisoryRequest):
# Look up the customer and generate personalized advice
customer = get_customer_by_id(request.customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
advice = generate_advice(customer, request.question)
return {"advice": advice, "customer_name": customer.name}
# Serve frontend files from the static directory
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/")
def serve_frontend():
return FileResponse("static/index.html")
What do these endpoints do?
- GET /health returns a simple status check to confirm the server is running.
- GET /customers returns a simplified list of customer profiles for the frontend dropdown.
- GET /customers/{customer_id} returns a full profile with calculated metrics for a specific customer.
- POST /advice takes a customer ID and a question, then returns personalized AI advice.
- The app.mount line and root route will serve the frontend files you create in the next step.
- Save main.py.
- Start the server by running this command:
uvicorn main:app --reload
- Open your browser and go to http://127.0.0.1:8000/health.
- You should see a JSON response: {"status":"healthy","service":"WealthAI Advisory"}.
FastAPI's interactive docs
FastAPI automatically generates API documentation. Visit http://127.0.0.1:8000/docs to see all your endpoints in a Swagger UI where you can test them directly.
Server won't start?
- If you see a ModuleNotFoundError, make sure your virtual environment is activated and you ran pip install for all dependencies in Step 1.
- If you see a RuntimeError about the static directory, make sure you created the empty static/ folder with mkdir static.
- If you see an import error for advisory_engine, check that advisory_engine.py is saved in the same directory as main.py.
help me fix my FastAPI server startup error for the WealthAI project
✔️ Awesome, I've got everything!
Great. Double check you've saved main.py and the server is running.
ⓧ I'd like to double check the full code
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from models import AdvisoryRequest
from data_service import get_customer_by_id, load_customers, calculate_portfolio_metrics
from advisory_engine import generate_advice
app = FastAPI(title="WealthAI Advisory API")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health")
def health_check():
return {"status": "healthy", "service": "WealthAI Advisory"}
@app.get("/customers")
def list_customers():
customers = load_customers()
return [
{"id": c.id, "name": c.name, "age": c.age, "income": c.income}
for c in customers
]
@app.get("/customers/{customer_id}")
def get_customer(customer_id: str):
customer = get_customer_by_id(customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
metrics = calculate_portfolio_metrics(customer)
return {"profile": customer.model_dump(), "metrics": metrics}
@app.post("/advice")
def get_advice(request: AdvisoryRequest):
customer = get_customer_by_id(request.customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
advice = generate_advice(customer, request.question)
return {"advice": advice, "customer_name": customer.name}
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/")
def serve_frontend():
return FileResponse("static/index.html")
Your AI advisory engine is live and generating personalized guidance. Next up, you'll build the animated avatar frontend that brings these responses to life in a conversational interface.
Build the Avatar Frontend
Your advisory engine can now analyze customer profiles and generate personalized financial guidance. The missing piece is a visual interface that makes this AI advice feel like a conversation with a real advisor.
In this step, you'll build a mobile-app-style frontend with an animated avatar that pulses when "speaking." The interface uses a dark theme with chat bubbles, mimicking the kind of experience customers expect from modern banking apps.
In this step, get ready to:
- Create the HTML layout for the mobile-app-style interface.
- Style the avatar with CSS animations for a pulsing speaking effect.
- Write the JavaScript that connects the frontend to your backend API.
Create the HTML layout
The HTML file defines the structure of your advisory interface. It includes a header, an animated avatar area, a customer profile dropdown, a chat message display, and a text input for questions.
- Create the static/ directory inside your project folder by running this command:
mkdir static
- Confirm the directory was created by running:
ls
You should see static listed alongside your existing files like main.py and advisory_engine.py.
- Create a new file called index.html inside the static/ folder and paste the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WealthAI Advisor</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="app-container">
<header class="app-header">
<h1>WealthAI Advisor</h1>
<p class="subtitle">Personalized Wealth Guidance</p>
</header>
<div class="avatar-section">
<div class="avatar" id="avatar">
<div class="avatar-face">W</div>
<div class="avatar-glow"></div>
</div>
<p class="avatar-status" id="avatar-status">Select a customer to begin</p>
</div>
<div class="customer-select">
<select id="customer-dropdown">
<option value="">-- Select Customer Profile --</option>
</select>
</div>
<div class="chat-area" id="chat-area">
</div>
<div class="input-area">
<input type="text" id="question-input" placeholder="Ask for financial advice..." disabled>
<button id="send-btn" disabled>Send</button>
</div>
</div>
<script src="/static/app.js"></script>
</body>
</html>
What does this HTML do?
The layout is structured like a mobile banking app with a fixed-width container (.app-container) that keeps everything centered and readable.
- The avatar div contains two layers: avatar-face (the visible circle with "W") and avatar-glow (the pulsing animation layer behind it).
- The customer-dropdown select starts with a placeholder option. JavaScript will populate it with actual customer names from the API.
- The input and button start disabled because the user must select a customer profile before asking questions.
- Save static/index.html.
Style the interface with CSS animations
The CSS provides a dark banking-app theme and the avatar's signature pulsing glow animation. When the avatar is "speaking," a glowing ring behind it scales up and down to show activity.
- Create a new file called style.css inside the static/ folder.
- Add the base styles, header, and avatar animation by pasting this code:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #0f1923;
color: #e0e0e0;
min-height: 100vh;
}
.app-container {
max-width: 480px;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
background: #1a2632;
}
.app-header {
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #1e3a5f, #2d5a87);
border-bottom: 1px solid #2d5a87;
}
.app-header h1 {
font-size: 1.4rem;
color: #ffffff;
font-weight: 600;
}
.subtitle {
font-size: 0.8rem;
color: #8bb4d4;
margin-top: 4px;
}
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
}
.avatar {
position: relative;
width: 80px;
height: 80px;
}
.avatar-face {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, #2d5a87, #4a90d9);
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
font-weight: bold;
color: #ffffff;
position: relative;
z-index: 2;
}
.avatar-glow {
position: absolute;
top: -4px;
left: -4px;
width: 88px;
height: 88px;
border-radius: 50%;
background: rgba(74, 144, 217, 0.3);
z-index: 1;
opacity: 0;
transition: opacity 0.3s ease;
}
.avatar.speaking .avatar-glow {
opacity: 1;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 0.3; }
50% { transform: scale(1.15); opacity: 0.6; }
}
.avatar-status {
margin-top: 12px;
font-size: 0.85rem;
color: #8bb4d4;
}
How does the avatar animation work?
The animation uses two layers. The .avatar-glow div sits behind the face circle and starts invisible (opacity: 0).
When JavaScript adds the speaking class to the avatar, the @keyframes pulse animation kicks in. The glow ring scales from 1x to 1.15x and back, creating the pulsing effect that signals the advisor is "thinking."
- Now add the chat area and input styles below the avatar styles in the same file:
.customer-select {
padding: 0 20px 16px;
}
.customer-select select {
width: 100%;
padding: 12px;
border-radius: 8px;
border: 1px solid #2d5a87;
background: #0f1923;
color: #e0e0e0;
font-size: 0.9rem;
}
.chat-area {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 12px;
min-height: 200px;
}
.message {
max-width: 85%;
padding: 12px 16px;
border-radius: 16px;
font-size: 0.9rem;
line-height: 1.5;
white-space: pre-wrap;
}
.message.user {
align-self: flex-end;
background: #2d5a87;
color: #ffffff;
border-bottom-right-radius: 4px;
}
.message.advisor {
align-self: flex-start;
background: #1e3a5f;
color: #e0e0e0;
border-bottom-left-radius: 4px;
border: 1px solid #2d5a87;
}
.message.loading {
color: #8bb4d4;
font-style: italic;
}
.input-area {
display: flex;
gap: 8px;
padding: 16px 20px;
border-top: 1px solid #2d5a87;
background: #0f1923;
}
.input-area input {
flex: 1;
padding: 12px 16px;
border-radius: 24px;
border: 1px solid #2d5a87;
background: #1a2632;
color: #e0e0e0;
font-size: 0.9rem;
outline: none;
}
.input-area input:focus {
border-color: #4a90d9;
}
.input-area input:disabled {
opacity: 0.5;
}
.input-area button {
padding: 12px 20px;
border-radius: 24px;
border: none;
background: linear-gradient(135deg, #2d5a87, #4a90d9);
color: #ffffff;
font-weight: 600;
cursor: pointer;
font-size: 0.9rem;
}
.input-area button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.input-area button:hover:not(:disabled) {
background: linear-gradient(135deg, #4a90d9, #6ab0f0);
}
What do these styles do?
- The .message.user and .message.advisor classes create the chat bubble effect. User messages align right with a solid blue background. Advisor messages align left with a bordered darker background.
- The border-bottom-right-radius: 4px on user messages creates the pointed corner effect that makes bubbles look like speech balloons.
- The input and button use pill-shaped borders (border-radius: 24px) for a modern mobile-app feel.
- Save static/style.css.
✔️ Awesome, I've got everything!
Great. Make sure you've saved static/style.css with both chunks combined into one file.
ⓧ I'd like to double check the full code
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #0f1923;
color: #e0e0e0;
min-height: 100vh;
}
.app-container {
max-width: 480px;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
background: #1a2632;
}
.app-header {
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #1e3a5f, #2d5a87);
border-bottom: 1px solid #2d5a87;
}
.app-header h1 {
font-size: 1.4rem;
color: #ffffff;
font-weight: 600;
}
.subtitle {
font-size: 0.8rem;
color: #8bb4d4;
margin-top: 4px;
}
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
}
.avatar {
position: relative;
width: 80px;
height: 80px;
}
.avatar-face {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, #2d5a87, #4a90d9);
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
font-weight: bold;
color: #ffffff;
position: relative;
z-index: 2;
}
.avatar-glow {
position: absolute;
top: -4px;
left: -4px;
width: 88px;
height: 88px;
border-radius: 50%;
background: rgba(74, 144, 217, 0.3);
z-index: 1;
opacity: 0;
transition: opacity 0.3s ease;
}
.avatar.speaking .avatar-glow {
opacity: 1;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 0.3; }
50% { transform: scale(1.15); opacity: 0.6; }
}
.avatar-status {
margin-top: 12px;
font-size: 0.85rem;
color: #8bb4d4;
}
.customer-select {
padding: 0 20px 16px;
}
.customer-select select {
width: 100%;
padding: 12px;
border-radius: 8px;
border: 1px solid #2d5a87;
background: #0f1923;
color: #e0e0e0;
font-size: 0.9rem;
}
.chat-area {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 12px;
min-height: 200px;
}
.message {
max-width: 85%;
padding: 12px 16px;
border-radius: 16px;
font-size: 0.9rem;
line-height: 1.5;
white-space: pre-wrap;
}
.message.user {
align-self: flex-end;
background: #2d5a87;
color: #ffffff;
border-bottom-right-radius: 4px;
}
.message.advisor {
align-self: flex-start;
background: #1e3a5f;
color: #e0e0e0;
border-bottom-left-radius: 4px;
border: 1px solid #2d5a87;
}
.message.loading {
color: #8bb4d4;
font-style: italic;
}
.input-area {
display: flex;
gap: 8px;
padding: 16px 20px;
border-top: 1px solid #2d5a87;
background: #0f1923;
}
.input-area input {
flex: 1;
padding: 12px 16px;
border-radius: 24px;
border: 1px solid #2d5a87;
background: #1a2632;
color: #e0e0e0;
font-size: 0.9rem;
outline: none;
}
.input-area input:focus {
border-color: #4a90d9;
}
.input-area input:disabled {
opacity: 0.5;
}
.input-area button {
padding: 12px 20px;
border-radius: 24px;
border: none;
background: linear-gradient(135deg, #2d5a87, #4a90d9);
color: #ffffff;
font-weight: 600;
cursor: pointer;
font-size: 0.9rem;
}
.input-area button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.input-area button:hover:not(:disabled) {
background: linear-gradient(135deg, #4a90d9, #6ab0f0);
}
Wire up the JavaScript interaction
The JavaScript handles three responsibilities: loading customer profiles from the API, sending advisory questions to the backend, and animating the avatar while the response arrives.
- Create a new file called app.js inside the static/ folder.
- Add the DOM references, customer loading function, and dropdown handler by pasting this code:
const customerDropdown = document.getElementById("customer-dropdown");
const questionInput = document.getElementById("question-input");
const sendBtn = document.getElementById("send-btn");
const chatArea = document.getElementById("chat-area");
const avatar = document.getElementById("avatar");
const avatarStatus = document.getElementById("avatar-status");
let selectedCustomerId = null;
// Fetch customer list from the API and populate the dropdown
async function loadCustomers() {
const response = await fetch("/customers");
const customers = await response.json();
customers.forEach(function (customer) {
const option = document.createElement("option");
option.value = customer.id;
option.textContent = customer.name + " (Age: " + customer.age + ", Income: $" + customer.income.toLocaleString() + ")";
customerDropdown.appendChild(option);
});
}
// When a customer is selected, enable the chat and show a welcome message
customerDropdown.addEventListener("change", function () {
selectedCustomerId = customerDropdown.value;
if (selectedCustomerId) {
questionInput.disabled = false;
sendBtn.disabled = false;
avatarStatus.textContent = "Ready to assist. Ask me anything!";
chatArea.innerHTML = "";
addMessage("advisor", "Hello! I have reviewed your financial profile. How can I help you today? You can ask me about investment strategies, spending optimization, retirement planning, or any other financial topic.");
} else {
questionInput.disabled = true;
sendBtn.disabled = true;
avatarStatus.textContent = "Select a customer to begin";
}
});
What does this code do?
- The first six const lines grab references to the HTML elements by their IDs so the script can update them dynamically.
- The loadCustomers() function calls the GET /customers endpoint you built in Step 3 and creates dropdown options for each customer.
- The change listener enables the input field when a customer is selected and posts a welcome message from the advisor.
- Add the message display function, the question-sending logic, and event listeners below the code you just pasted:
// Create a chat bubble and append it to the chat area
function addMessage(role, text) {
const messageDiv = document.createElement("div");
messageDiv.className = "message " + role;
messageDiv.textContent = text;
chatArea.appendChild(messageDiv);
chatArea.scrollTop = chatArea.scrollHeight;
return messageDiv;
}
// Send the user's question to the advisory API
async function sendQuestion() {
const question = questionInput.value.trim();
if (!question || !selectedCustomerId) return;
addMessage("user", question);
questionInput.value = "";
// Activate the speaking animation while waiting for a response
avatar.classList.add("speaking");
avatarStatus.textContent = "Analyzing your profile...";
const loadingMsg = addMessage("advisor", "Thinking...");
loadingMsg.classList.add("loading");
sendBtn.disabled = true;
questionInput.disabled = true;
try {
const response = await fetch("/advice", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
customer_id: selectedCustomerId,
question: question,
}),
});
const data = await response.json();
chatArea.removeChild(loadingMsg);
addMessage("advisor", data.advice);
} catch (error) {
chatArea.removeChild(loadingMsg);
addMessage("advisor", "I apologize, but I encountered an issue processing your request. Please try again.");
} finally {
// Stop the speaking animation and re-enable input
avatar.classList.remove("speaking");
avatarStatus.textContent = "Ready to assist. Ask me anything!";
sendBtn.disabled = false;
questionInput.disabled = false;
questionInput.focus();
}
}
// Wire up the send button and Enter key
sendBtn.addEventListener("click", sendQuestion);
questionInput.addEventListener("keydown", function (e) {
if (e.key === "Enter") sendQuestion();
});
// Load customers when the page first opens
loadCustomers();
What does this code do?
- The addMessage() function creates a div with the appropriate CSS class (message user or message advisor) and scrolls the chat to the bottom.
- The sendQuestion() function adds the speaking class to the avatar (triggering the CSS pulse), posts the question to POST /advice, and displays the response as a new chat bubble.
- The finally block always runs after the API call. It removes the speaking animation and re-enables input regardless of whether the call succeeded or failed.
- Save static/app.js.
- Confirm all three frontend files exist by running:
ls static/
You should see app.js, index.html, and style.css listed in the output.
Missing a file?
If you only see one or two files, make sure you created them inside the static/ directory, not in the project root. Check that each file was saved with the correct extension (.html, .css, .js).
Help me figure out why my static files are missing.
✔️ Awesome, I've got everything!
Great. Double check you've saved all three files inside static/.
ⓧ I'd like to double check the full code
Here is the complete static/app.js file:
const customerDropdown = document.getElementById("customer-dropdown");
const questionInput = document.getElementById("question-input");
const sendBtn = document.getElementById("send-btn");
const chatArea = document.getElementById("chat-area");
const avatar = document.getElementById("avatar");
const avatarStatus = document.getElementById("avatar-status");
let selectedCustomerId = null;
async function loadCustomers() {
const response = await fetch("/customers");
const customers = await response.json();
customers.forEach(function (customer) {
const option = document.createElement("option");
option.value = customer.id;
option.textContent = customer.name + " (Age: " + customer.age + ", Income: $" + customer.income.toLocaleString() + ")";
customerDropdown.appendChild(option);
});
}
customerDropdown.addEventListener("change", function () {
selectedCustomerId = customerDropdown.value;
if (selectedCustomerId) {
questionInput.disabled = false;
sendBtn.disabled = false;
avatarStatus.textContent = "Ready to assist. Ask me anything!";
chatArea.innerHTML = "";
addMessage("advisor", "Hello! I have reviewed your financial profile. How can I help you today? You can ask me about investment strategies, spending optimization, retirement planning, or any other financial topic.");
} else {
questionInput.disabled = true;
sendBtn.disabled = true;
avatarStatus.textContent = "Select a customer to begin";
}
});
function addMessage(role, text) {
const messageDiv = document.createElement("div");
messageDiv.className = "message " + role;
messageDiv.textContent = text;
chatArea.appendChild(messageDiv);
chatArea.scrollTop = chatArea.scrollHeight;
return messageDiv;
}
async function sendQuestion() {
const question = questionInput.value.trim();
if (!question || !selectedCustomerId) return;
addMessage("user", question);
questionInput.value = "";
avatar.classList.add("speaking");
avatarStatus.textContent = "Analyzing your profile...";
const loadingMsg = addMessage("advisor", "Thinking...");
loadingMsg.classList.add("loading");
sendBtn.disabled = true;
questionInput.disabled = true;
try {
const response = await fetch("/advice", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
customer_id: selectedCustomerId,
question: question,
}),
});
const data = await response.json();
chatArea.removeChild(loadingMsg);
addMessage("advisor", data.advice);
} catch (error) {
chatArea.removeChild(loadingMsg);
addMessage("advisor", "I apologize, but I encountered an issue processing your request. Please try again.");
} finally {
avatar.classList.remove("speaking");
avatarStatus.textContent = "Ready to assist. Ask me anything!";
sendBtn.disabled = false;
questionInput.disabled = false;
questionInput.focus();
}
}
sendBtn.addEventListener("click", sendQuestion);
questionInput.addEventListener("keydown", function (e) {
if (e.key === "Enter") sendQuestion();
});
loadCustomers();
Why CSS-only animations instead of a 3D avatar?
Production banking apps like Morgan Stanley's advisor tools often use simple animated indicators rather than full 3D rendering. A pulsing glow communicates "the advisor is working" without adding GPU requirements, extra API costs, or complex dependencies.
This approach keeps your prototype lightweight and focused on what matters: the quality of the AI-generated advice, not the visual fidelity of a 3D face.
Your frontend files are ready. Next up, you'll connect them to your FastAPI backend and run the full system end-to-end.
Connect and Demo the Full System
You have a backend that generates personalized financial advice and a frontend that displays it in a chat interface. The missing piece is connecting them so the browser can load the frontend directly from the FastAPI server.
In this step, you will configure FastAPI to serve your static files, launch the full application, and test the end-to-end experience with all three customer profiles.
In this step, get ready to:
- Configure FastAPI to serve the frontend at the root URL.
- Run the full application and verify the avatar animates during responses.
- Test personalization across different customer profiles and question types.
Configure FastAPI to serve static files
Your frontend files live in the static/ directory, but FastAPI does not serve them automatically. You need to mount that directory and add a root route so visiting http://127.0.0.1:8000 loads your app.
- In main.py, scroll to the bottom of the file (after the get_advice endpoint).
- Add the static file mount and root route by copying this code at the end of the file:
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/")
def serve_frontend():
return FileResponse("static/index.html")
What does this code do?
- app.mount("/static", ...) tells FastAPI to serve any file inside the static/ directory when a browser requests a URL starting with /static/. This is how your CSS and JavaScript files get delivered.
- The @app.get("/") route catches requests to the root URL and returns your index.html using FileResponse. This is what makes the frontend load when you open the app in a browser.
Why does the mount come after the API routes?
FastAPI matches routes in the order they are defined. Placing app.mount after your API endpoints ensures that /customers, /advice, and /health are handled by your API logic first, not by the static file server.
- Save main.py.
Seeing an import error?
Check that your imports at the top of main.py include from fastapi.staticfiles import StaticFiles and from fastapi.responses import FileResponse. Both were added in an earlier step.
help me fix an import error in my FastAPI main.py
✔️ Awesome, I've got everything!
Great. Double check you have saved main.py before moving on.
ⓧ I'd like to double check the full code
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from models import AdvisoryRequest
from data_service import get_customer_by_id, load_customers, calculate_portfolio_metrics
from advisory_engine import generate_advice
app = FastAPI(title="WealthAI Advisory API")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health")
def health_check():
return {"status": "healthy", "service": "WealthAI Advisory"}
@app.get("/customers")
def list_customers():
customers = load_customers()
return [
{"id": c.id, "name": c.name, "age": c.age, "income": c.income}
for c in customers
]
@app.get("/customers/{customer_id}")
def get_customer(customer_id: str):
customer = get_customer_by_id(customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
metrics = calculate_portfolio_metrics(customer)
return {"profile": customer.model_dump(), "metrics": metrics}
@app.post("/advice")
def get_advice(request: AdvisoryRequest):
customer = get_customer_by_id(request.customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
advice = generate_advice(customer, request.question)
return {"advice": advice, "customer_name": customer.name}
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/")
def serve_frontend():
return FileResponse("static/index.html")
Run the full application
Everything is wired up. Time to launch the server and see the full system in action.
- Start the application by running this command in your terminal:
uvicorn main:app --reload
What does this command do?
- uvicorn is the ASGI server that runs your FastAPI app.
- main:app tells it to look in main.py for the object called app.
- --reload makes the server restart automatically whenever you save a file. Useful during development.
- Open your browser and navigate to http://127.0.0.1:8000.
- You should see the WealthAI Advisor interface with a dark background, a circular avatar with the letter "W", and a customer dropdown.
- Select Sarah Chen from the customer dropdown.
- Type How should I invest for retirement given my current portfolio? in the input field and click Send.
Watch the avatar. The circular glow around it should pulse while the AI generates a response. After a few seconds, the response appears in a chat bubble on the left.
Not seeing the interface?
- Check your terminal for error messages. A missing .env file or invalid API key will cause a startup failure.
- Make sure you are in the project directory (the folder containing main.py) when running the uvicorn command.
- If the page loads but the dropdown is empty, check your browser console for errors. The /customers endpoint might have an issue loading data/customers.json.
help me debug why my WealthAI app is not loading in the browser
Test personalization across customer profiles
The real power of this system is that different customers get different advice based on their actual financial data. Try these comparisons to see it in action.
- With Sarah Chen selected (age 29, risk tolerance 8/10), ask:
- What percentage of my portfolio should be in equities?
- Now switch to Michael Torres (age 52, risk tolerance 4/10) and ask the same question.
- Notice how the AI gives different recommendations. Sarah gets more aggressive guidance because of her higher risk tolerance and younger age. Michael gets more conservative advice emphasizing wealth preservation.
- Try a spending question with Priya Sharma selected:
- How can I optimize my spending to save more for my children's education?
- The response should reference Priya's actual $1,200/month education spending and her $2,500/month savings rate. The AI uses her real numbers because your system prompt injects them.
Why does each customer get different advice?
Your format_customer_context() function in advisory_engine.py injects each customer's actual spending amounts, portfolio allocations, and risk tolerance into the system prompt. The AI model sees these specific numbers and tailors its response accordingly.
This is the core architecture pattern behind production wealth advisory systems. The model stays the same, but the context changes per customer.
Your AI wealth advisory prototype is complete. You have a backend that personalizes advice using real customer data, a frontend with an animated avatar, and a working end-to-end demo that proves the concept. Next up is the Secret Mission, where you will add proactive risk scoring.
Secret mission
Add a Portfolio Risk Scoring Engine
Your advisor waits to be asked before it speaks up. But a real wealth advisor spots danger and warns you proactively. Can you build a risk scoring engine that detects portfolio concentration and has the avatar alert customers without them asking?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine. The Anthropic API uses prepaid credits, so there are no ongoing charges while your server is stopped. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The running uvicorn server (local process).
- Your project folder on the Desktop (Python files, virtual environment, static assets).
- Your Anthropic API account with prepaid credits at console.anthropic.com.
- Your .env file containing your API key (inside the project folder).
Keep everything running
No action needed. Choose this if you're still actively building or want to continue experimenting with different customer profiles and advisory questions.
- Your project files stay on your Desktop.
- The uvicorn server only runs when you start it manually. It does not consume resources when stopped.
- Your Anthropic credits remain in your account and are only consumed when you make API calls.
Pause - I'll come back to this later
Stop the running server to free up your terminal. Your files and account remain intact for when you return.
- In the terminal where uvicorn is running, press Ctrl+C to stop the server.
- Your project folder and virtual environment stay on your Desktop, ready to reactivate later.
- Your Anthropic API key and credits remain active.
Delete - I don't want to use this again
Remove all project files and optionally delete your Anthropic account. This cannot be undone.
- In the terminal where uvicorn is running, press Ctrl+C to stop the server.
- Delete the project folder by running this command (macOS/Linux):
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]
- On Windows, run this instead:
cd ~\Desktop
rmdir /s /q [[PROJECT_FOLDER="your project folder name"]]
- To delete your Anthropic Console account, go to console.anthropic.com and navigate to Settings then Organization.
- Click the red Delete organization button and follow the confirmation prompts.
Want to keep the account but revoke the key?
If you want to keep your Anthropic account but remove the API key used in this project, go to console.anthropic.com and navigate to Settings then API Keys. Delete the key you created for this project. Your remaining credits stay in your account for future use.
Nice Work!
Nice Work!
Nice work! You just built a fully functional AI-powered wealth advisory avatar that delivers personalized financial guidance based on real customer data.
This is the same core architecture pattern that firms like Morgan Stanley are deploying with Claude today. You now have hands-on proof that you can build it.
What you learned:
- Built a FastAPI backend that ingests customer financial profiles and calls Claude Haiku to generate personalized investment recommendations.
- Engineered a system prompt that produces compliant, data-driven financial guidance by injecting specific portfolio numbers, spending breakdowns, and risk tolerance into the model's context.
- Created an animated avatar frontend with CSS pulsing animations, chat-style message bubbles, and a mobile-app-style layout that brings the advisory experience to life.
- Secret Mission: Added a proactive risk scoring engine that calculates portfolio concentration risk and has the avatar warn customers without being asked.
Ready to quiz yourself?