Lots of changes

This commit is contained in:
James Ketr 2025-05-24 10:38:13 -07:00
parent e36ed818fb
commit 2a83118689
5 changed files with 273 additions and 83 deletions

View File

@ -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<CandidateInfoProps> = (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 <Box>No user loaded.</Box>;
}
return (
<StyledPaper sx={sx}>
<Grid container spacing={2}>
<Card
elevation={1}
sx={{
display: "flex",
borderColor: 'transparent',
borderWidth: 2,
borderStyle: 'solid',
transition: 'all 0.3s ease',
...sx
}}
>
<CardActionArea
//onClick={() => setSelectedCandidate(candidate)}
sx={{ height: '100%', display: 'flex', flexDirection: 'column', alignItems: 'stretch' }}
>
<CardContent sx={{ flexGrow: 1, p: 3 }}>
<Grid container spacing={2}>
<Grid size={{ xs: 12, sm: 2 }} sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minWidth: "80px", maxWidth: "80px" }}>
<Avatar
src={view.has_profile ? `/api/u/${view.username}/profile/${sessionId}?timestamp=${Date.now()}` : ''}
alt={`${view.full_name}'s profile`}
src={candidate.has_profile ? `/api/u/${candidate.username}/profile/${sessionId}?timestamp=${Date.now()}` : ''}
alt={`${candidate.full_name}'s profile`}
sx={{
width: 80,
height: 80,
@ -68,20 +90,20 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row", alignItems: "center", gap: 1, "& > .MuiTypography-root": { m: 0 } }}>
{action !== '' && <Typography variant="body1">{action}</Typography>}
<Typography variant="h5" component="h1" sx={{ fontWeight: 'bold', whiteSpace: 'nowrap' }}>
{view.full_name}
{candidate.full_name}
</Typography>
</Box>
<Box sx={{ fontSize: "0.75rem", alignItems: "center" }} >
<Link href={`/u/${view.username}`}>/u/{view.username}</Link>
<Link href={`/u/${candidate.username}`}>/u/{candidate.username}</Link>
<CopyBubble
onClick={(event: any) => { event.stopPropagation() }}
tooltip="Copy link" content={`${window.location.origin}/u/{view.username}`} />
tooltip="Copy link" content={`${window.location.origin}/u/{candidate.username}`} />
</Box>
</Box>
{view.rag_content_size !== undefined && view.rag_content_size > 0 && <Chip
{candidate.rag_content_size !== undefined && candidate.rag_content_size > 0 && <Chip
onClick={(event: React.MouseEvent<HTMLDivElement>) => { 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<CandidateInfoProps> = (props: CandidateInfoProps)
</Box>
<Typography variant="body1" color="text.secondary">
{view.description}
{candidate.description}
</Typography>
<Divider sx={{ my: 2 }} />
<Typography variant="body2" sx={{ mb: 1 }}>
<strong>Location:</strong> {candidate.location}
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
<strong>Email:</strong> {candidate.email}
</Typography>
<Typography variant="body2">
<strong>Phone:</strong> {candidate.phone}
</Typography>
</Grid>
</Grid>
</Grid>
</StyledPaper>
</CardContent>
</CardActionArea>
</Card>
);
};

View File

@ -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,

View File

@ -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 (
<Box>
<Box sx={{display: "flex", flexDirection: "column"}}>
<Box sx={{ p: 1 }}>
Not seeing a candidate you like?
<Button
variant="contained"
sx={{ml: 1}}
onClick={() => { navigate('/generate-candidate')}}>
Generate your own perfect AI candidate!
</Button>
</Box>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap"}}>
{users?.map((u, i) =>
<Box key={`${u.username}`}
onClick={(event: React.MouseEvent<HTMLDivElement>) : void => {
@ -70,10 +81,11 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
}}
sx={{ cursor: "pointer" }}
>
<CandidateInfo sessionId={sessionId} sx={{ "cursor": "pointer", "&:hover": { border: "2px solid orange" }, border: "2px solid transparent" }} user={u} />
<CandidateInfo sessionId={sessionId} sx={{ maxWidth: "320px", "cursor": "pointer", "&:hover": { border: "2px solid orange" }, border: "2px solid transparent" }} user={u} />
</Box>
)}
</Box>
</Box>
);
};

View File

@ -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 (
<Box className="GenerateCandidate" sx={{
display: "flex", flexDirection: "column", flexGrow: 1, gap: 1, width: { xs: '100%', md: '700px', lg: '1024px' }
}}>
{user && <CandidateInfo sessionId={sessionId} user={user} />}
{processing &&
<Box sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
m: 2,
}}>
<Box sx={{flexDirection: "row", fontWeight: "bold"}}>{status}</Box>
<PropagateLoader
size="10px"
loading={processing}
aria-label="Loading Spinner"
data-testid="loader"
/>
<Box className="GenerateCandidate" sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
gap: 1, width: { xs: '100%', md: '700px', lg: '1024px' }
}}>
{user && <CandidateInfo
sessionId={sessionId}
user={user}
sx={{flexShrink: 1}}/>
}
{ prompt &&
<Box sx={{ display: "flex", flexDirection: "column"}}>
<Box sx={{ fontSize: "0.5rem"}}>Persona prompt</Box>
<Box sx={{ fontWeight: "bold"}}>{prompt}</Box>
</Box>
}
{processing &&
<Box sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
m: 2,
}}>
{ status && <Box sx={{ display: "flex", flexDirection: "column"}}>
<Box sx={{ fontSize: "0.5rem"}}>Generation status</Box>
<Box sx={{ fontWeight: "bold"}}>{status}</Box>
</Box>}
<PropagateLoader
size="10px"
loading={processing}
aria-label="Loading Spinner"
data-testid="loader"
/>
</Box>
}
<Box sx={{display: "flex", flexDirection: "column"}}>
<Box sx={{ display: "flex", flexDirection: "row", position: "relative" }}>
<Box sx={{ display: "flex", position: "relative" }}>
<Avatar
src={user.has_profile ? `/api/u/${user.username}/profile/${sessionId}` : ''}
alt={`${user.full_name}'s profile`}
sx={{
width: 80,
height: 80,
border: '2px solid #e0e0e0',
}}
/>
{processing && <Pulse sx={{ position: "relative", left: "-80px", top: "0px" }} timestamp={timestamp} />}
</Box>
<Tooltip title={"Generate Picture"}>
<span style={{ display: "flex", flexGrow: 1 }}>
<Button
sx={{ m: 1, gap: 1, flexGrow: 1 }}
variant="contained"
disabled={sessionId === undefined || processing || !canGenImage}
onClick={() => { setShouldGenerateProfile(true); }}>
Generate Picture<SendIcon />
</Button>
</span>
</Tooltip>
<Box sx={{ display: "flex", flexDirection: "row", position: "relative" }}>
<Box sx={{ display: "flex", position: "relative" }}>
<Avatar
src={user.has_profile ? `/api/u/${user.username}/profile/${sessionId}` : ''}
alt={`${user.full_name}'s profile`}
sx={{
width: 80,
height: 80,
border: '2px solid #e0e0e0',
}}
/>
{processing && <Pulse sx={{ position: "relative", left: "-80px", top: "0px" }} timestamp={timestamp} />}
</Box>
<Tooltip title={"Generate Picture"}>
<span style={{ display: "flex", flexGrow: 1 }}>
<Button
sx={{ m: 1, gap: 1, flexGrow: 1 }}
variant="contained"
disabled={sessionId === undefined || processing || !canGenImage}
onClick={() => { setShouldGenerateProfile(true); }}>
Generate Picture<SendIcon />
</Button>
</span>
</Tooltip>
</Box>
</Box>
{ resume !== '' && <Paper sx={{pt: 1, pb: 1, pl: 2, pr: 2}}><Scrollable sx={{flexGrow: 1}}><StyledMarkdown {...{content: resume, setSnack, sessionId, submitQuery}}/></Scrollable></Paper> }
{ resume !== '' &&
<Paper sx={{pt: 1, pb: 1, pl: 2, pr: 2}}>
<Scrollable sx={{flexGrow: 1}}>
<StyledMarkdown {...{content: resume, setSnack, sessionId, submitQuery}}/>
</Scrollable>
</Paper> }
<BackstoryTextField
style={{ flexGrow: 0, flexShrink: 1 }}
ref={backstoryTextRef}
@ -343,6 +368,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
</span>
</Tooltip>
</Box>
<Box sx={{display: "flex", flexGrow: 1}}/>
</Box>);
};

View File

@ -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"]}
}}
```
"""