sykang16/agentic-wealth-intelligence
0
1# UI Design & Structure2 3## Overview4 5This document outlines the user interface architecture for the Agentic Wealth Intelligence System. The UI serves as the primary interaction layer between users and the multi-agent backend, supporting both single-user demos and multi-user deployments.6 7---8 9## UI Strategy10 11### Development Approach: Progressive Enhancement12 13We adopt a **progressive enhancement strategy** that balances speed with sophistication:14 15```16Phase 1 (Week 1) → Phase 2 (Weeks 2-3) → Phase 3 (Weeks 4-6)17───────────────── ────────────────── ──────────────────18CLI Interface Streamlit Web App Multi-User Support19(Validation) (MVP/Demo) (Authentication & Isolation)20```21 22**Recommendation**: Focus on **Streamlit** (Phase 2) for demo, then add **multi-user support** (Phase 3).23 24---25 26## Phase 1: CLI Interface (Validation)27 28### Purpose29- Quick validation of agent logic30- No UI overhead31- Developer-friendly testing32 33### Implementation34 35```python36# src/cli/main.py37from rich.console import Console38from rich.prompt import Prompt39from rich.panel import Panel40from rich.markdown import Markdown41 42console = Console()43 44def main():45 console.print(Panel.fit(46 "[bold blue]Agentic Wealth Intelligence[/bold blue]\n"47 "Multi-Agent Financial Advisory System",48 border_style="blue"49 ))50 51 while True:52 user_input = Prompt.ask("\n[bold green]You[/bold green]")53 54 if user_input.lower() in ['exit', 'quit']:55 break56 57 # Call orchestrator agent58 console.print("[dim]Thinking...[/dim]")59 response = await orchestrator_agent.run(user_input)60 61 # Display response62 console.print(f"\n[bold cyan]Assistant ({response.agent})[/bold cyan]")63 console.print(Markdown(response.content))64 65 # Show any visualizations66 if response.chart_data:67 console.print("[dim]Chart data available (view in UI)[/dim]")68 69if __name__ == "__main__":70 import asyncio71 asyncio.run(main())72```73 74### Features75✅ Simple text-based interaction76✅ Color-coded output (Rich library)77✅ Markdown rendering for responses78✅ Quick iteration and testing79 80---81 82## Phase 2: Streamlit Web App (Recommended MVP)83 84 85### Streamlit Application Structure86 87```88ui/89├── streamlit_app.py # Main entry point90├── pages/91│ ├── 1_Dashboard.py # Portfolio overview92│ ├── 2_Chat.py # Agent conversation93│ ├── 3_Profile.py # Investment profile94│ └── 4_Recommendations.py # Product recommendations95├── components/96│ ├── __init__.py97│ ├── auth.py # Authentication UI98│ ├── charts.py # Visualization helpers99│ ├── chat_ui.py # Chat components100│ ├── portfolio_cards.py # Dashboard widgets101│ └── metrics.py # Financial metrics display102├── utils/103│ ├── __init__.py104│ ├── api_client.py # Backend API calls105│ ├── state_manager.py # Session state106│ ├── auth_manager.py # User authentication107│ └── formatters.py # Data formatting108└── config.py # UI configuration109```110 111---112 113### Main Application Entry Point114 115```python116# ui/streamlit_app.py117import streamlit as st118from utils.state_manager import initialize_session_state119from utils.auth_manager import check_authentication120from utils.api_client import APIClient121 122# Page configuration123st.set_page_config(124 page_title="Agentic Wealth Intelligence",125 page_icon="💰",126 layout="wide",127 initial_sidebar_state="expanded"128)129 130# Check authentication131if not check_authentication():132 st.stop() # Will show login page133 134# Initialize session state135initialize_session_state()136 137# Get current user info138user_info = st.session_state.user_info139 140# Sidebar141with st.sidebar:142 st.title("💰 Wealth Intelligence")143 st.divider()144 145 # User info146 st.subheader(f"👤 {user_info['name']}")147 st.caption(f"ID: {user_info['user_id']}")148 149 # Logout button150 if st.button("🚪 Logout"):151 st.session_state.clear()152 st.rerun()153 154 st.divider()155 156 # Quick stats157 col1, col2 = st.columns(2)158 with col1:159 st.metric("Net Worth", "$125,430", "+5.2%")160 with col2:161 st.metric("Portfolio", "$98,450", "+3.1%")162 163 st.divider()164 165 # Navigation info166 st.caption("📊 Dashboard - Overview")167 st.caption("💬 Chat - Talk to AI")168 st.caption("👤 Profile - Your preferences")169 st.caption("💡 Recommendations - Personalized advice")170 171# Main content172st.title(f"Welcome back, {user_info['name']}!")173 174st.markdown("""175This multi-agent AI system helps you understand your finances and make informed investment decisions.176 177### Getting Started1781. **📊 Dashboard**: View your complete financial picture1792. **💬 Chat**: Ask questions about your portfolio1803. **👤 Profile**: Complete your investment profile1814. **💡 Recommendations**: Get personalized advice182""")183 184# Quick actions185col1, col2, col3 = st.columns(3)186 187with col1:188 if st.button("📊 View Dashboard", use_container_width=True):189 st.switch_page("pages/1_Dashboard.py")190 191with col2:192 if st.button("💬 Start Chat", use_container_width=True):193 st.switch_page("pages/2_Chat.py")194 195with col3:196 if st.button("💡 Get Advice", use_container_width=True):197 st.switch_page("pages/4_Recommendations.py")198```199 200---201 202### Page 1: Dashboard (Portfolio Overview)203 204```python205# ui/pages/1_Dashboard.py206import streamlit as st207import plotly.express as px208import plotly.graph_objects as go209from components.charts import create_allocation_chart, create_performance_chart210from components.portfolio_cards import render_account_cards211from utils.api_client import APIClient212from utils.auth_manager import require_authentication213 214st.set_page_config(page_title="Dashboard", page_icon="📊", layout="wide")215 216# Require authentication217require_authentication()218 219st.title("📊 Portfolio Dashboard")220 221# Initialize API client with user context222api_client = APIClient(user_id=st.session_state.user_info['user_id'])223 224# Fetch portfolio data225with st.spinner("Loading portfolio data..."):226 portfolio = api_client.get_portfolio()227 228# Top metrics row229col1, col2, col3, col4 = st.columns(4)230 231with col1:232 st.metric(233 "Total Assets",234 f"${portfolio.total_assets:,.2f}",235 f"{portfolio.asset_change_pct:+.1%}"236 )237 238with col2:239 st.metric(240 "Net Worth",241 f"${portfolio.net_worth:,.2f}",242 f"{portfolio.net_worth_change_pct:+.1%}"243 )244 245with col3:246 st.metric(247 "Liquidity Ratio",248 f"{portfolio.liquidity_ratio:.2f}",249 "Healthy" if portfolio.liquidity_ratio > 0.5 else "Low",250 delta_color="normal" if portfolio.liquidity_ratio > 0.5 else "inverse"251 )252 253with col4:254 st.metric(255 "Monthly Cashflow",256 f"${portfolio.monthly_cashflow:,.2f}",257 f"{portfolio.cashflow_change:+.1%}"258 )259 260st.divider()261 262# Charts row263col1, col2 = st.columns(2)264 265with col1:266 st.subheader("Asset Allocation")267 fig = create_allocation_chart(portfolio.allocation)268 st.plotly_chart(fig, use_container_width=True)269 270with col2:271 st.subheader("Portfolio Performance (6 Months)")272 fig = create_performance_chart(portfolio.performance_history)273 st.plotly_chart(fig, use_container_width=True)274 275st.divider()276 277# Accounts section278st.subheader("Accounts Overview")279render_account_cards(portfolio.accounts)280 281st.divider()282 283# Interactive query section284st.subheader("💬 Ask About Your Portfolio")285 286col1, col2 = st.columns([3, 1])287 288with col1:289 query = st.text_input(290 "Natural language query",291 placeholder="e.g., What's my debt-to-income ratio?",292 label_visibility="collapsed"293 )294 295with col2:296 if st.button("Ask Asset Agent", use_container_width=True):297 if query:298 with st.spinner("Analyzing..."):299 response = api_client.query_asset_agent(query)300 st.info(response.answer)301 302 # Show any generated charts303 if response.chart:304 st.plotly_chart(response.chart)305```306 307---308 309### Page 2: Chat Interface (Multi-Agent Conversation)310 311```python312# ui/pages/2_Chat.py313import streamlit as st314from utils.api_client import APIClient315from components.chat_ui import render_message, show_agent_indicator316from utils.auth_manager import require_authentication317 318st.set_page_config(page_title="Chat", page_icon="💬", layout="wide")319 320# Require authentication321require_authentication()322 323st.title("💬 Chat with AI Agents")324 325# Initialize chat history per user326chat_key = f"messages_{st.session_state.user_info['user_id']}"327if chat_key not in st.session_state:328 st.session_state[chat_key] = []329 st.session_state[chat_key].append({330 "role": "assistant",331 "content": f"Hello {st.session_state.user_info['name']}! I'm your wealth intelligence assistant. I can help you understand your finances, complete your investment profile, or provide personalized recommendations. What would you like to know?",332 "agent": "orchestrator"333 })334 335# API client336api_client = APIClient(user_id=st.session_state.user_info['user_id'])337 338# Display chat messages339for message in st.session_state[chat_key]:340 with st.chat_message(message["role"]):341 # Show which agent is responding342 if message["role"] == "assistant":343 show_agent_indicator(message.get("agent", "orchestrator"))344 345 st.markdown(message["content"])346 347 # Display any charts or data348 if "chart" in message:349 st.plotly_chart(message["chart"], use_container_width=True)350 351 if "data" in message:352 with st.expander("View data"):353 st.json(message["data"])354 355# Chat input356if prompt := st.chat_input("Type your message here..."):357 # Add user message358 st.session_state[chat_key].append({359 "role": "user",360 "content": prompt361 })362 363 # Display user message364 with st.chat_message("user"):365 st.markdown(prompt)366 367 # Get agent response368 with st.chat_message("assistant"):369 with st.spinner("Thinking..."):370 response = api_client.chat(371 message=prompt,372 history=st.session_state[chat_key]373 )374 375 # Show active agent376 show_agent_indicator(response.agent)377 378 # Stream response (if supported)379 st.markdown(response.content)380 381 # Show any visualizations382 if response.chart:383 st.plotly_chart(response.chart, use_container_width=True)384 385 # Add assistant message to history386 st.session_state[chat_key].append({387 "role": "assistant",388 "content": response.content,389 "agent": response.agent,390 "chart": response.chart if hasattr(response, 'chart') else None391 })392 393# Sidebar: Conversation controls394with st.sidebar:395 st.subheader("Conversation")396 397 if st.button("🔄 New Conversation"):398 st.session_state[chat_key] = []399 st.rerun()400 401 if st.button("💾 Save Conversation"):402 # Save to file or database403 st.success("Conversation saved!")404 405 st.divider()406 407 # Show active agent context408 st.caption("**Active Context**")409 st.caption(f"Messages: {len(st.session_state[chat_key])}")410 411 # Agent activity412 if st.session_state[chat_key]:413 last_agent = st.session_state[chat_key][-1].get("agent", "unknown")414 st.caption(f"Last agent: {last_agent}")415```416 417---418 419### Page 3: Investment Profile420 421```python422# ui/pages/3_Profile.py423import streamlit as st424from utils.api_client import APIClient425from utils.auth_manager import require_authentication426 427st.set_page_config(page_title="Profile", page_icon="👤", layout="wide")428 429# Require authentication430require_authentication()431 432st.title("👤 Investment Profile")433 434api_client = APIClient(user_id=st.session_state.user_info['user_id'])435 436# Tabs for different modes437tab1, tab2 = st.tabs(["📝 Conversational Profiling", "📋 View Profile"])438 439with tab1:440 st.subheader("Complete Your Profile Through Conversation")441 st.info("💡 Our AI agent will ask you questions to understand your investment preferences. Just answer naturally!")442 443 # Profiling session key per user444 profiling_key = f"profiling_{st.session_state.user_info['user_id']}"445 446 # Start profiling conversation447 if st.button("🚀 Start Profiling Conversation", type="primary"):448 # Initialize profiling session449 st.session_state[profiling_key] = {450 "active": True,451 "messages": []452 }453 st.rerun()454 455 # Profiling conversation456 if st.session_state.get(profiling_key, {}).get("active", False):457 # Get profiling status458 status = api_client.get_profiling_status()459 460 # Progress indicator461 progress = status.filled_slots / status.total_slots462 st.progress(progress, text=f"Profile Completion: {progress*100:.0f}%")463 464 # Show which slots are filled465 with st.expander("Profile Status"):466 col1, col2 = st.columns(2)467 with col1:468 st.success(f"✅ Completed: {', '.join(status.filled_slots)}")469 with col2:470 st.warning(f"⏳ Remaining: {', '.join(status.missing_slots)}")471 472 st.divider()473 474 # Conversation interface (similar to chat)475 for msg in st.session_state[profiling_key]["messages"]:476 with st.chat_message(msg["role"]):477 st.markdown(msg["content"])478 479 # User input480 if response := st.chat_input("Your answer..."):481 # Add to messages482 st.session_state[profiling_key]["messages"].append({483 "role": "user",484 "content": response485 })486 487 # Send to profiling agent488 with st.spinner("Processing..."):489 agent_response = api_client.profiling_agent_step(response)490 491 st.session_state[profiling_key]["messages"].append({492 "role": "assistant",493 "content": agent_response.question or agent_response.summary494 })495 496 # Check if complete497 if agent_response.is_complete:498 st.session_state[profiling_key]["active"] = False499 st.success("✅ Profile completed!")500 st.balloons()501 502 st.rerun()503 504with tab2:505 st.subheader("Your Investment Profile")506 507 # Fetch current profile508 profile = api_client.get_profile()509 510 if profile:511 # Display profile in organized sections512 col1, col2 = st.columns(2)513 514 with col1:515 st.markdown("### Risk Assessment")516 st.metric("Risk Tolerance", profile.risk_tolerance.title())517 st.metric("Loss Comfort (1-10)", profile.loss_comfort)518 519 st.markdown("### Time Horizon")520 st.metric("Investment Period", profile.investment_period.title())521 st.text_area("Liquidity Needs", profile.liquidity_needs, disabled=True)522 523 with col2:524 st.markdown("### Financial Situation")525 st.metric("Income Stability", profile.income_stability.title())526 st.metric("Emergency Fund", "Yes" if profile.emergency_fund else "No")527 st.metric("Debt Level", profile.debt_level.title())528 529 st.markdown("### Experience")530 st.metric("Level", profile.experience_level.title())531 st.text_area("Previous Investments", 532 ", ".join(profile.previous_investments), 533 disabled=True)534 535 st.divider()536 537 st.markdown("### Investment Goals")538 st.text_area("Primary Goal", profile.primary_goal, disabled=True)539 if profile.target_return:540 st.metric("Target Return", f"{profile.target_return}%")541 542 # Edit button543 if st.button("✏️ Update Profile"):544 st.session_state[profiling_key] = {"active": True, "messages": []}545 st.rerun()546 547 else:548 st.warning("No profile found. Please complete the conversational profiling.")549```550 551---552 553### Page 4: Recommendations554 555```python556# ui/pages/4_Recommendations.py557import streamlit as st558from utils.api_client import APIClient559from utils.auth_manager import require_authentication560 561st.set_page_config(page_title="Recommendations", page_icon="💡", layout="wide")562 563# Require authentication564require_authentication()565 566st.title("💡 Personalized Investment Recommendations")567 568api_client = APIClient(user_id=st.session_state.user_info['user_id'])569 570# Check if profile exists571profile = api_client.get_profile()572if not profile:573 st.warning("⚠️ Please complete your investment profile first.")574 if st.button("Go to Profile"):575 st.switch_page("pages/3_👤_Profile.py")576 st.stop()577 578# Get recommendations579with st.spinner("Generating personalized recommendations..."):580 recommendations = api_client.get_recommendations()581 582# Display recommendations583st.subheader(f"Based on your {profile.risk_tolerance} risk profile")584 585for idx, rec in enumerate(recommendations):586 with st.container():587 col1, col2 = st.columns([2, 1])588 589 with col1:590 st.markdown(f"### {idx+1}. {rec.product_name}")591 st.caption(f"{rec.product_type} | Ticker: {rec.ticker}")592 st.markdown(rec.description)593 594 # Rationale595 with st.expander("Why this recommendation?"):596 st.markdown(rec.rationale)597 st.caption(f"**Sources**: {', '.join(rec.sources)}")598 599 with col2:600 # Metrics601 st.metric("Expected Return", f"{rec.expected_return}%")602 st.metric("Risk Level", rec.risk_level.title())603 st.metric("Allocation", f"{rec.suggested_allocation}%")604 605 # Action buttons606 if st.button("📊 View Details", key=f"details_{idx}"):607 st.session_state.selected_product = rec.ticker608 # Show detailed modal609 610 if st.button("💬 Ask About This", key=f"ask_{idx}"):611 st.session_state.prefill_chat = f"Tell me more about {rec.product_name}"612 st.switch_page("pages/2_💬_Chat.py")613 614 st.divider()615 616# Market context617with st.expander("📰 Current Market Context"):618 st.markdown("### Recent Market News")619 for news in recommendations.market_context:620 st.markdown(f"- **{news.headline}** ({news.source})")621 st.caption(news.summary)622 623# Refresh button624if st.button("🔄 Refresh Recommendations"):625 st.rerun()626```627 628---629 630### Component Examples631 632```python633# ui/components/charts.py634import plotly.express as px635import plotly.graph_objects as go636 637def create_allocation_chart(allocation_data):638 """Create pie chart for asset allocation"""639 fig = px.pie(640 values=allocation_data.values(),641 names=allocation_data.keys(),642 title="Asset Allocation",643 hole=0.4644 )645 fig.update_traces(textposition='inside', textinfo='percent+label')646 return fig647 648def create_performance_chart(performance_history):649 """Create line chart for portfolio performance"""650 fig = go.Figure()651 652 fig.add_trace(go.Scatter(653 x=performance_history.dates,654 y=performance_history.values,655 mode='lines+markers',656 name='Portfolio Value',657 line=dict(color='#1f77b4', width=2)658 ))659 660 fig.update_layout(661 title="Portfolio Performance",662 xaxis_title="Date",663 yaxis_title="Value ($)",664 hovermode='x unified'665 )666 667 return fig668```669 670```python671# ui/components/chat_ui.py672import streamlit as st673 674def show_agent_indicator(agent_name):675 """Show which agent is responding"""676 agent_colors = {677 "orchestrator": "🎯",678 "asset_agent": "📊",679 "profiling_agent": "👤",680 "advisory_agent": "💡"681 }682 683 agent_labels = {684 "orchestrator": "Orchestrator",685 "asset_agent": "Asset Analyst",686 "profiling_agent": "Profile Guide",687 "advisory_agent": "Investment Advisor"688 }689 690 icon = agent_colors.get(agent_name, "🤖")691 label = agent_labels.get(agent_name, agent_name.title())692 693 st.caption(f"{icon} **{label}** is responding")694```695 696---697 698## Phase 3: Multi-User Support699 700### Overview701 702Phase 3 adds proper user management, authentication, and data isolation to support multiple users accessing the system simultaneously.703 704### Architecture Components705 706```707┌─────────────────────────────────────────────────┐708│ Frontend (Streamlit) │709│ - Login/Registration UI │710│ - Session Management │711│ - User Context │712└────────────────┬────────────────────────────────┘713 │714 │ HTTP/WebSocket715 │716┌────────────────▼────────────────────────────────┐717│ Backend API (FastAPI) │718│ - User Authentication │719│ - JWT Token Management │720│ - User Context Middleware │721└────────────────┬────────────────────────────────┘722 │723 ┌────────┴────────┐724 │ │725┌───────▼──────┐ ┌──────▼────────┐726│ User DB │ │ User Data DB │727│ (Auth) │ │ (Portfolios, │728│ │ │ Profiles) │729└──────────────┘ └───────────────┘730```731 732---733 734### User Authentication System735 736#### Database Schema737 738```python739# backend/src/auth/models.py740from sqlalchemy import Column, String, DateTime, Boolean741from sqlalchemy.orm import declarative_base742import uuid743from datetime import datetime744 745Base = declarative_base()746 747class User(Base):748 __tablename__ = "users"749 750 user_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))751 email = Column(String, unique=True, nullable=False)752 username = Column(String, unique=True, nullable=False)753 hashed_password = Column(String, nullable=False)754 full_name = Column(String)755 756 created_at = Column(DateTime, default=datetime.utcnow)757 last_login = Column(DateTime)758 is_active = Column(Boolean, default=True)759 is_verified = Column(Boolean, default=False)760 761 # Preferences762 theme = Column(String, default="light")763 language = Column(String, default="en")764 765class UserSession(Base):766 __tablename__ = "user_sessions"767 768 session_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))769 user_id = Column(String, nullable=False)770 access_token = Column(String, nullable=False)771 refresh_token = Column(String)772 773 created_at = Column(DateTime, default=datetime.utcnow)774 expires_at = Column(DateTime, nullable=False)775 last_activity = Column(DateTime, default=datetime.utcnow)776 777 ip_address = Column(String)778 user_agent = Column(String)779```780 781#### Authentication Backend782 783```python784# backend/src/auth/auth_service.py785from passlib.context import CryptContext786from jose import JWTError, jwt787from datetime import datetime, timedelta788from typing import Optional789 790pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")791 792SECRET_KEY = "your-secret-key-here" # Use environment variable793ALGORITHM = "HS256"794ACCESS_TOKEN_EXPIRE_MINUTES = 30795 796class AuthService:797 @staticmethod798 def hash_password(password: str) -> str:799 """Hash a password"""800 return pwd_context.hash(password)801 802 @staticmethod803 def verify_password(plain_password: str, hashed_password: str) -> bool:804 """Verify a password against hash"""805 return pwd_context.verify(plain_password, hashed_password)806 807 @staticmethod808 def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):809 """Create JWT access token"""810 to_encode = data.copy()811 812 if expires_delta:813 expire = datetime.utcnow() + expires_delta814 else:815 expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)816 817 to_encode.update({"exp": expire})818 encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)819 return encoded_jwt820 821 @staticmethod822 def verify_token(token: str) -> Optional[dict]:823 """Verify and decode JWT token"""824 try:825 payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])826 return payload827 except JWTError:828 return None829 830 async def register_user(831 self, 832 email: str, 833 username: str, 834 password: str,835 full_name: str836 ) -> User:837 """Register a new user"""838 # Check if user exists839 existing_user = await self.get_user_by_email(email)840 if existing_user:841 raise ValueError("User with this email already exists")842 843 # Create user844 hashed_password = self.hash_password(password)845 user = User(846 email=email,847 username=username,848 hashed_password=hashed_password,849 full_name=full_name850 )851 852 # Save to database853 await self.db.save(user)854 return user855 856 async def authenticate_user(self, email: str, password: str) -> Optional[User]:857 """Authenticate user with email and password"""858 user = await self.get_user_by_email(email)859 860 if not user:861 return None862 863 if not self.verify_password(password, user.hashed_password):864 return None865 866 # Update last login867 user.last_login = datetime.utcnow()868 await self.db.save(user)869 870 return user871```872 873#### Authentication Endpoints874 875```python876# backend/src/api/routes/auth.py877from fastapi import APIRouter, Depends, HTTPException, status878from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm879from pydantic import BaseModel, EmailStr880 881router = APIRouter(prefix="/api/v1/auth", tags=["authentication"])882 883oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")884 885class UserRegister(BaseModel):886 email: EmailStr887 username: str888 password: str889 full_name: str890 891class UserLogin(BaseModel):892 email: EmailStr893 password: str894 895class Token(BaseModel):896 access_token: str897 token_type: str898 user_info: dict899 900@router.post("/register", response_model=Token)901async def register(user_data: UserRegister, auth_service: AuthService = Depends()):902 """Register a new user"""903 try:904 user = await auth_service.register_user(905 email=user_data.email,906 username=user_data.username,907 password=user_data.password,908 full_name=user_data.full_name909 )910 911 # Create access token912 access_token = auth_service.create_access_token(913 data={"sub": user.email, "user_id": user.user_id}914 )915 916 return Token(917 access_token=access_token,918 token_type="bearer",919 user_info={920 "user_id": user.user_id,921 "email": user.email,922 "username": user.username,923 "full_name": user.full_name924 }925 )926 927 except ValueError as e:928 raise HTTPException(929 status_code=status.HTTP_400_BAD_REQUEST,930 detail=str(e)931 )932 933@router.post("/login", response_model=Token)934async def login(935 form_data: OAuth2PasswordRequestForm = Depends(),936 auth_service: AuthService = Depends()937):938 """Login user"""939 user = await auth_service.authenticate_user(940 email=form_data.username, # Using username field for email941 password=form_data.password942 )943 944 if not user:945 raise HTTPException(946 status_code=status.HTTP_401_UNAUTHORIZED,947 detail="Incorrect email or password",948 headers={"WWW-Authenticate": "Bearer"},949 )950 951 # Create access token952 access_token = auth_service.create_access_token(953 data={"sub": user.email, "user_id": user.user_id}954 )955 956 return Token(957 access_token=access_token,958 token_type="bearer",959 user_info={960 "user_id": user.user_id,961 "email": user.email,962 "username": user.username,963 "full_name": user.full_name964 }965 )966 967@router.get("/me")968async def get_current_user(token: str = Depends(oauth2_scheme), auth_service: AuthService = Depends()):969 """Get current user info"""970 payload = auth_service.verify_token(token)971 972 if not payload:973 raise HTTPException(974 status_code=status.HTTP_401_UNAUTHORIZED,975 detail="Could not validate credentials",976 headers={"WWW-Authenticate": "Bearer"},977 )978 979 user = await auth_service.get_user_by_id(payload["user_id"])980 981 if not user:982 raise HTTPException(status_code=404, detail="User not found")983 984 return {985 "user_id": user.user_id,986 "email": user.email,987 "username": user.username,988 "full_name": user.full_name989 }990 991@router.post("/logout")992async def logout(token: str = Depends(oauth2_scheme)):993 """Logout user (invalidate token)"""994 # In production, add token to blacklist995 return {"message": "Successfully logged out"}996```997 998---999 1000### Frontend Authentication Components1001 1002```python1003# ui/components/auth.py1004import streamlit as st1005import requests1006from typing import Optional1007 1008API_BASE_URL = "http://localhost:8000"1009 1010def show_login_page():1011 """Display login page"""1012 st.title("🔐 Login to Wealth Intelligence")1013 1014 tab1, tab2 = st.tabs(["Login", "Register"])1015 1016 with tab1:1017 with st.form("login_form"):1018 st.subheader("Welcome Back")1019 email = st.text_input("Email")1020 password = st.text_input("Password", type="password")1021 1022 col1, col2 = st.columns(2)1023 with col1:1024 submit = st.form_submit_button("Login", use_container_width=True)1025 with col2:1026 if st.form_submit_button("Forgot Password?", use_container_width=True):1027 st.info("Password reset feature coming soon!")1028 1029 if submit:1030 if login_user(email, password):1031 st.success("Login successful!")1032 st.rerun()1033 else:1034 st.error("Invalid email or password")1035 1036 with tab2:1037 with st.form("register_form"):1038 st.subheader("Create Account")1039 full_name = st.text_input("Full Name")1040 username = st.text_input("Username")1041 email = st.text_input("Email")1042 password = st.text_input("Password", type="password")1043 password_confirm = st.text_input("Confirm Password", type="password")1044 1045 submit = st.form_submit_button("Create Account", use_container_width=True)1046 1047 if submit:1048 if password != password_confirm:1049 st.error("Passwords don't match")1050 elif len(password) < 8:1051 st.error("Password must be at least 8 characters")1052 else:1053 if register_user(email, username, password, full_name):1054 st.success("Account created! You can now login.")1055 else:1056 st.error("Registration failed. Email may already exist.")1057 1058def login_user(email: str, password: str) -> bool:1059 """Login user and store token"""1060 try:1061 response = requests.post(1062 f"{API_BASE_URL}/api/v1/auth/login",1063 data={"username": email, "password": password} # OAuth2 uses 'username'1064 )1065 1066 if response.status_code == 200:1067 data = response.json()1068 1069 # Store in session state1070 st.session_state.access_token = data["access_token"]1071 st.session_state.user_info = data["user_info"]1072 st.session_state.authenticated = True1073 1074 return True1075 1076 return False1077 1078 except Exception as e:1079 st.error(f"Login error: {str(e)}")1080 return False1081 1082def register_user(email: str, username: str, password: str, full_name: str) -> bool:1083 """Register new user"""1084 try:1085 response = requests.post(1086 f"{API_BASE_URL}/api/v1/auth/register",1087 json={1088 "email": email,1089 "username": username,1090 "password": password,1091 "full_name": full_name1092 }1093 )1094 1095 if response.status_code == 200:1096 return True1097 1098 return False1099 1100 except Exception as e:1101 st.error(f"Registration error: {str(e)}")1102 return False1103```1104 1105```python1106# ui/utils/auth_manager.py1107import streamlit as st1108from components.auth import show_login_page1109import requests1110 1111API_BASE_URL = "http://localhost:8000"1112 1113def check_authentication() -> bool:1114 """Check if user is authenticated"""1115 if not st.session_state.get("authenticated", False):1116 show_login_page()1117 return False1118 1119 # Verify token is still valid1120 if not verify_token():1121 st.session_state.authenticated = False1122 st.warning("Session expired. Please login again.")1123 show_login_page()1124 return False1125 1126 return True1127 1128def require_authentication():1129 """Decorator-like function to require authentication"""1130 if not check_authentication():1131 st.stop()1132 1133def verify_token() -> bool:1134 """Verify access token is still valid"""1135 if "access_token" not in st.session_state:1136 return False1137 1138 try:1139 response = requests.get(1140 f"{API_BASE_URL}/api/v1/auth/me",1141 headers={"Authorization": f"Bearer {st.session_state.access_token}"}1142 )1143 1144 return response.status_code == 2001145 1146 except:1147 return False1148 1149def logout():1150 """Logout current user"""1151 if "access_token" in st.session_state:1152 try:1153 requests.post(1154 f"{API_BASE_URL}/api/v1/auth/logout",1155 headers={"Authorization": f"Bearer {st.session_state.access_token}"}1156 )1157 except:1158 pass1159 1160 # Clear session1161 st.session_state.clear()1162```1163 1164---1165 1166### Data Isolation1167 1168#### User-Scoped Data Storage1169 1170```python1171# backend/src/data/user_data.py1172from typing import Optional1173from datetime import datetime1174 1175class UserDataManager:1176 """Manage user-specific data with proper isolation"""1177 1178 def __init__(self, db_session):1179 self.db = db_session1180 1181 async def get_user_portfolio(self, user_id: str) -> Optional[AssetPortfolio]:1182 """Get portfolio for specific user only"""1183 portfolio = await self.db.query(AssetPortfolio).filter(1184 AssetPortfolio.user_id == user_id1185 ).first()1186 1187 return portfolio1188 1189 async def get_user_profile(self, user_id: str) -> Optional[InvestmentProfile]:1190 """Get investment profile for specific user only"""1191 profile = await self.db.query(InvestmentProfile).filter(1192 InvestmentProfile.user_id == user_id1193 ).first()1194 1195 return profile1196 1197 async def save_user_data(self, user_id: str, data: dict):1198 """Save data for specific user"""1199 # Always verify user_id matches1200 if data.get("user_id") != user_id: