diff --git a/frontend/src/NewApp/Components/CandidateInfo.tsx b/frontend/src/NewApp/Components/CandidateInfo.tsx index 2dc06ad..8284c62 100644 --- a/frontend/src/NewApp/Components/CandidateInfo.tsx +++ b/frontend/src/NewApp/Components/CandidateInfo.tsx @@ -1,9 +1,15 @@ import React from 'react'; import { Box, Link, Typography, Avatar, Paper, Grid, Chip, SxProps } from '@mui/material'; +import { + Card, + CardContent, + CardActionArea, + Divider, + useTheme, +} from '@mui/material'; import { useMediaQuery } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; import { styled } from '@mui/material/styles'; -import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { UserInfo, useUser } from "./UserContext"; import LinkIcon from '@mui/icons-material/Link'; import { CopyBubble } from "../../Components/CopyBubble"; @@ -42,18 +48,34 @@ const CandidateInfo: React.FC = (props: CandidateInfoProps) if (size < 1000000) return `${(size / 1000).toFixed(1)}K RAG elements`; return `${(size / 1000000).toFixed(1)}M RAG elements`; }; - const view = props.user || user; + const candidate = props.user || user; - if (!view) { + if (!candidate) { return No user loaded.; } return ( - - + + setSelectedCandidate(candidate)} + sx={{ height: '100%', display: 'flex', flexDirection: 'column', alignItems: 'stretch' }} + > + + + = (props: CandidateInfoProps) .MuiTypography-root": { m: 0 } }}> {action !== '' && {action}} - {view.full_name} + {candidate.full_name} - /u/{view.username} + /u/{candidate.username} { event.stopPropagation() }} - tooltip="Copy link" content={`${window.location.origin}/u/{view.username}`} /> + tooltip="Copy link" content={`${window.location.origin}/u/{candidate.username}`} /> - {view.rag_content_size !== undefined && view.rag_content_size > 0 && 0 && ) => { navigate('/knowledge-explorer'); event.stopPropagation() }} - label={formatRagSize(view.rag_content_size)} + label={formatRagSize(candidate.rag_content_size)} color="primary" size="small" sx={{ ml: 2 }} @@ -89,11 +111,26 @@ const CandidateInfo: React.FC = (props: CandidateInfoProps) - {view.description} + {candidate.description} + + + + + Location: {candidate.location} + + + Email: {candidate.email} + + + Phone: {candidate.phone} + + + - - + + + ); }; diff --git a/frontend/src/NewApp/Components/UserContext.tsx b/frontend/src/NewApp/Components/UserContext.tsx index a8c25cb..a0b25ba 100644 --- a/frontend/src/NewApp/Components/UserContext.tsx +++ b/frontend/src/NewApp/Components/UserContext.tsx @@ -21,6 +21,10 @@ interface UserInfo { questions: UserQuestion[], isAuthenticated: boolean, has_profile: boolean, + title: string; + location: string; + email: string; + phone: string; // Fields used in AI generated personas age?: number, ethnicity?: string, diff --git a/frontend/src/NewApp/Pages/CandidateListingPage.tsx b/frontend/src/NewApp/Pages/CandidateListingPage.tsx index 700ec8f..4f70c0a 100644 --- a/frontend/src/NewApp/Pages/CandidateListingPage.tsx +++ b/frontend/src/NewApp/Pages/CandidateListingPage.tsx @@ -1,5 +1,6 @@ import React, { forwardRef, useEffect, useState, MouseEventHandler } from 'react'; import { useNavigate } from "react-router-dom"; +import Button from '@mui/material/Button'; import useMediaQuery from '@mui/material/useMediaQuery'; import Box from '@mui/material/Box'; import { useTheme } from '@mui/material/styles'; @@ -62,7 +63,17 @@ const CandidateListingPage = (props: BackstoryPageProps) => { }, [users]); return ( - + + + Not seeing a candidate you like? + + + {users?.map((u, i) => ) : void => { @@ -70,10 +81,11 @@ const CandidateListingPage = (props: BackstoryPageProps) => { }} sx={{ cursor: "pointer" }} > - + )} + ); }; diff --git a/frontend/src/NewApp/Pages/GenerateCandidate.tsx b/frontend/src/NewApp/Pages/GenerateCandidate.tsx index ecfbe0f..e62b62c 100644 --- a/frontend/src/NewApp/Pages/GenerateCandidate.tsx +++ b/frontend/src/NewApp/Pages/GenerateCandidate.tsx @@ -33,7 +33,11 @@ const emptyUser : UserInfo = { contact_info: {}, questions: [], isAuthenticated: false, - has_profile: false + has_profile: false, + title: '[blank]', + location: '[blank]', + email: '[blank]', + phone: '[blank]', }; const GenerateCandidate = (props: BackstoryElementProps) => { @@ -167,7 +171,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => { } const imagePrompt = `A photorealistic profile picture of a ${user.age} year old ${user.gender} ${user.ethnicity} person. ${prompt}` - setStatus(`Generating: ${imagePrompt}`); + setStatus('Staring image generation...'); setProcessing(true); setCanGenImage(false); setState(3); @@ -260,55 +264,76 @@ const GenerateCandidate = (props: BackstoryElementProps) => { } return ( - - {user && } - {processing && - - {status} - + + {user && + } + { prompt && + + Persona prompt + {prompt} } + {processing && + + { status && + Generation status + {status} + } + + + } - - - - {processing && } - - - - - - + + + + {processing && } + + + + + + - { resume !== '' && } + { resume !== '' && + + + + + } { + ); }; diff --git a/src/utils/agents/persona_generator.py b/src/utils/agents/persona_generator.py index 3729134..ad6de57 100644 --- a/src/utils/agents/persona_generator.py +++ b/src/utils/agents/persona_generator.py @@ -21,6 +21,8 @@ import time import asyncio import time import os +import random +from names_dataset import NameDataset, NameWrapper # type: ignore from .base import Agent, agent_registry, LLMMessage from ..message import Message @@ -42,6 +44,9 @@ emptyUser = { "first_name": "", "last_name": "", "full_name": "", + "email": "", + "phone": "", + "title": "", "contact_info": {}, "questions": [], } @@ -76,6 +81,9 @@ Provide all information in English ONLY, with no other commentary: "last_name": string, "description": string, # One to two sentence description of their job "location": string, # In the location, provide ALL of: City, State/Region, and Country +"phone": string, # Location appropriate phone number with area code +"email": string, # primary email address +"title": string, # Job title of their current job } ``` @@ -107,6 +115,117 @@ Use that information to invent a full career resume. Include sections such as: Provide the resume in Markdown format. DO NOT provide any commentary before or after the resume. """ +class EthnicNameGenerator: + def __init__(self): + self.nd = NameDataset() + + # US Census 2020 approximate ethnic distribution + self.ethnic_weights = { + 'White': 0.576, + 'Hispanic': 0.186, + 'Black': 0.134, + 'Asian': 0.062, + 'Native American': 0.013, + 'Pacific Islander': 0.003, + 'Mixed/Other': 0.026 + } + + # Map ethnicities to name origins/countries that approximate those populations + self.ethnic_name_mapping = { + 'White': ['United States', 'United Kingdom', 'Germany', 'Ireland', 'Italy', 'Poland'], + 'Hispanic': ['Mexico', 'Spain', 'Colombia', 'Peru', 'Argentina', 'Cuba', 'Puerto Rico'], + 'Black': ['United States'], # African American names + 'Asian': ['China', 'India', 'Philippines', 'Vietnam', 'Korea', 'Japan'], + 'Native American': ['United States'], # Limited options in dataset + 'Pacific Islander': ['United States'], # Limited options in dataset + 'Mixed/Other': ['United States'] # Default to US names + } + + def get_weighted_ethnicity(self): + """Select ethnicity based on US demographic weights""" + ethnicities = list(self.ethnic_weights.keys()) + weights = list(self.ethnic_weights.values()) + return random.choices(ethnicities, weights=weights)[0] + + def get_name_by_ethnicity(self, ethnicity, gender='random'): + """Generate a name based on ethnicity""" + if gender == 'random': + gender = random.choice(['Male', 'Female']) + + countries = self.ethnic_name_mapping.get(ethnicity, ['United States']) + + # Try to get names from the mapped countries + first_name = None + last_name = None + + for country in countries: + try: + # Get first names + if country in self.nd.first_names: + country_first_names = self.nd.first_names[country] + if gender.lower() in country_first_names: + gender_names = country_first_names[gender.lower()] + if gender_names: + first_name = random.choice(gender_names) + break + + except (KeyError, AttributeError): + continue + + # Fallback to US names if no ethnicity-specific name found + if not first_name: + try: + us_names = self.nd.first_names.get('United States', {}) + gender_names = us_names.get(gender.lower(), []) + if gender_names: + first_name = random.choice(gender_names) + else: + # Ultimate fallback + first_name = random.choice(['John', 'Jane', 'Michael', 'Sarah']) + except: + first_name = random.choice(['John', 'Jane', 'Michael', 'Sarah']) + + # Get last name + for country in countries: + try: + if country in self.nd.last_names and self.nd.last_names[country]: + last_name = random.choice(self.nd.last_names[country]) + break + except (KeyError, AttributeError): + continue + + # Fallback for last name + if not last_name: + try: + us_last_names = self.nd.last_names.get('United States', []) + if us_last_names: + last_name = random.choice(us_last_names) + else: + last_name = random.choice(['Smith', 'Johnson', 'Williams', 'Brown']) + except: + last_name = random.choice(['Smith', 'Johnson', 'Williams', 'Brown']) + + return first_name, last_name, ethnicity, gender + + def generate_random_name(self, gender='random'): + """Generate a random name with ethnicity based on US demographics""" + ethnicity = self.get_weighted_ethnicity() + return self.get_name_by_ethnicity(ethnicity, gender) + + def generate_multiple_names(self, count=10, gender='random'): + """Generate multiple random names""" + names = [] + for _ in range(count): + first, last, ethnicity, actual_gender = self.generate_random_name(gender) + names.append({ + 'full_name': f"{first} {last}", + 'first_name': first, + 'last_name': last, + 'ethnicity': ethnicity, + 'gender': actual_gender + }) + return names + class PersonaGenerator(Agent): agent_type: Literal["persona"] = "persona" # type: ignore _agent_type: ClassVar[str] = agent_type # Add this for registration @@ -115,32 +234,19 @@ class PersonaGenerator(Agent): system_prompt: str = generate_persona_system_prompt age: int = Field(default_factory=lambda: random.randint(22, 67)) gender: str = Field(default_factory=lambda: random.choice(["male", "female"])) - ethnicity: Literal[ - "Asian", "African", "Caucasian", "Hispanic/Latino", "Mixed/Multiracial" - ] = Field( - default_factory=lambda: random.choices( - ["Asian", "African", "Caucasian", "Hispanic/Latino", "Mixed/Multiracial"], - weights=[57.69, 15.38, 19.23, 5.77, 1.92], - k=1 - )[0] - ) username: str = "" + first_name: str = "" + last_name: str = "" + ethnicity: str = "" + generator: Any = Field(default=EthnicNameGenerator(), exclude=True) llm: Any = Field(default=None, exclude=True) model: str = Field(default=None, exclude=True) def randomize(self): self.age = random.randint(22, 67) - self.gender = random.choice(["male", "female"]) # Use random.choices with explicit type casting to satisfy Literal type - self.ethnicity = cast( - Literal["Asian", "African", "Caucasian", "Hispanic/Latino", "Mixed/Multiracial"], - random.choices( - ["Asian", "African", "Caucasian", "Hispanic/Latino", "Mixed/Multiracial"], - weights=[57.69, 15.38, 19.23, 5.77, 1.92], - k=1 - )[0] - ) + self.first_name, self.last_name, self.ethnicity, self.gender = self.generator.generate_random_name() async def prepare_message(self, message: Message) -> AsyncGenerator[Message, None]: logger.info(f"{self.agent_type} - {inspect.stack()[0].function}") @@ -152,6 +258,8 @@ class PersonaGenerator(Agent): message.tunables.enable_rag = False message.tunables.enable_context = False + self.randomize() + message.prompt = f"""\ ```json {json.dumps({ @@ -260,7 +368,10 @@ class PersonaGenerator(Agent): "full_name": "{persona["full_name"]}", "location": "{persona["location"]}", "age": {persona["age"]}, - "description": {persona["description"]} + "description": {persona["description"]}, + "title": {persona["title"]}, + "email": {persona["email"]}, + "phone": {persona["phone"]} }} ``` """