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 React from 'react';
import { Box, Link, Typography, Avatar, Paper, Grid, Chip, SxProps } from '@mui/material'; 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 { useMediaQuery } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { styled } 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 { UserInfo, useUser } from "./UserContext";
import LinkIcon from '@mui/icons-material/Link'; import LinkIcon from '@mui/icons-material/Link';
import { CopyBubble } from "../../Components/CopyBubble"; 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`; if (size < 1000000) return `${(size / 1000).toFixed(1)}K RAG elements`;
return `${(size / 1000000).toFixed(1)}M 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 <Box>No user loaded.</Box>;
} }
return ( return (
<StyledPaper sx={sx}> <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 container spacing={2}>
<Grid size={{ xs: 12, sm: 2 }} sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minWidth: "80px", maxWidth: "80px" }}> <Grid size={{ xs: 12, sm: 2 }} sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minWidth: "80px", maxWidth: "80px" }}>
<Avatar <Avatar
src={view.has_profile ? `/api/u/${view.username}/profile/${sessionId}?timestamp=${Date.now()}` : ''} src={candidate.has_profile ? `/api/u/${candidate.username}/profile/${sessionId}?timestamp=${Date.now()}` : ''}
alt={`${view.full_name}'s profile`} alt={`${candidate.full_name}'s profile`}
sx={{ sx={{
width: 80, width: 80,
height: 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 } }}> <Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row", alignItems: "center", gap: 1, "& > .MuiTypography-root": { m: 0 } }}>
{action !== '' && <Typography variant="body1">{action}</Typography>} {action !== '' && <Typography variant="body1">{action}</Typography>}
<Typography variant="h5" component="h1" sx={{ fontWeight: 'bold', whiteSpace: 'nowrap' }}> <Typography variant="h5" component="h1" sx={{ fontWeight: 'bold', whiteSpace: 'nowrap' }}>
{view.full_name} {candidate.full_name}
</Typography> </Typography>
</Box> </Box>
<Box sx={{ fontSize: "0.75rem", alignItems: "center" }} > <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 <CopyBubble
onClick={(event: any) => { event.stopPropagation() }} 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>
</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() }} onClick={(event: React.MouseEvent<HTMLDivElement>) => { navigate('/knowledge-explorer'); event.stopPropagation() }}
label={formatRagSize(view.rag_content_size)} label={formatRagSize(candidate.rag_content_size)}
color="primary" color="primary"
size="small" size="small"
sx={{ ml: 2 }} sx={{ ml: 2 }}
@ -89,11 +111,26 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
</Box> </Box>
<Typography variant="body1" color="text.secondary"> <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> </Typography>
</Grid> </Grid>
</Grid> </Grid>
</StyledPaper> </CardContent>
</CardActionArea>
</Card>
); );
}; };

View File

@ -21,6 +21,10 @@ interface UserInfo {
questions: UserQuestion[], questions: UserQuestion[],
isAuthenticated: boolean, isAuthenticated: boolean,
has_profile: boolean, has_profile: boolean,
title: string;
location: string;
email: string;
phone: string;
// Fields used in AI generated personas // Fields used in AI generated personas
age?: number, age?: number,
ethnicity?: string, ethnicity?: string,

View File

@ -1,5 +1,6 @@
import React, { forwardRef, useEffect, useState, MouseEventHandler } from 'react'; import React, { forwardRef, useEffect, useState, MouseEventHandler } from 'react';
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import Button from '@mui/material/Button';
import useMediaQuery from '@mui/material/useMediaQuery'; import useMediaQuery from '@mui/material/useMediaQuery';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
@ -62,7 +63,17 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
}, [users]); }, [users]);
return ( 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) => {users?.map((u, i) =>
<Box key={`${u.username}`} <Box key={`${u.username}`}
onClick={(event: React.MouseEvent<HTMLDivElement>) : void => { onClick={(event: React.MouseEvent<HTMLDivElement>) : void => {
@ -70,10 +81,11 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
}} }}
sx={{ cursor: "pointer" }} 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> </Box>
</Box>
); );
}; };

View File

@ -33,7 +33,11 @@ const emptyUser : UserInfo = {
contact_info: {}, contact_info: {},
questions: [], questions: [],
isAuthenticated: false, isAuthenticated: false,
has_profile: false has_profile: false,
title: '[blank]',
location: '[blank]',
email: '[blank]',
phone: '[blank]',
}; };
const GenerateCandidate = (props: BackstoryElementProps) => { 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}` 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); setProcessing(true);
setCanGenImage(false); setCanGenImage(false);
setState(3); setState(3);
@ -261,9 +265,22 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
return ( return (
<Box className="GenerateCandidate" sx={{ <Box className="GenerateCandidate" sx={{
display: "flex", flexDirection: "column", flexGrow: 1, gap: 1, width: { xs: '100%', md: '700px', lg: '1024px' } display: "flex",
flexDirection: "column",
flexGrow: 1,
gap: 1, width: { xs: '100%', md: '700px', lg: '1024px' }
}}> }}>
{user && <CandidateInfo sessionId={sessionId} user={user} />} {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 && {processing &&
<Box sx={{ <Box sx={{
display: "flex", display: "flex",
@ -272,7 +289,10 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
justifyContent: "center", justifyContent: "center",
m: 2, m: 2,
}}> }}>
<Box sx={{flexDirection: "row", fontWeight: "bold"}}>{status}</Box> { status && <Box sx={{ display: "flex", flexDirection: "column"}}>
<Box sx={{ fontSize: "0.5rem"}}>Generation status</Box>
<Box sx={{ fontWeight: "bold"}}>{status}</Box>
</Box>}
<PropagateLoader <PropagateLoader
size="10px" size="10px"
loading={processing} loading={processing}
@ -308,7 +328,12 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
</Tooltip> </Tooltip>
</Box> </Box>
</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 <BackstoryTextField
style={{ flexGrow: 0, flexShrink: 1 }} style={{ flexGrow: 0, flexShrink: 1 }}
ref={backstoryTextRef} ref={backstoryTextRef}
@ -343,6 +368,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
</span> </span>
</Tooltip> </Tooltip>
</Box> </Box>
<Box sx={{display: "flex", flexGrow: 1}}/>
</Box>); </Box>);
}; };

View File

@ -21,6 +21,8 @@ import time
import asyncio import asyncio
import time import time
import os import os
import random
from names_dataset import NameDataset, NameWrapper # type: ignore
from .base import Agent, agent_registry, LLMMessage from .base import Agent, agent_registry, LLMMessage
from ..message import Message from ..message import Message
@ -42,6 +44,9 @@ emptyUser = {
"first_name": "", "first_name": "",
"last_name": "", "last_name": "",
"full_name": "", "full_name": "",
"email": "",
"phone": "",
"title": "",
"contact_info": {}, "contact_info": {},
"questions": [], "questions": [],
} }
@ -76,6 +81,9 @@ Provide all information in English ONLY, with no other commentary:
"last_name": string, "last_name": string,
"description": string, # One to two sentence description of their job "description": string, # One to two sentence description of their job
"location": string, # In the location, provide ALL of: City, State/Region, and Country "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. 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): class PersonaGenerator(Agent):
agent_type: Literal["persona"] = "persona" # type: ignore agent_type: Literal["persona"] = "persona" # type: ignore
_agent_type: ClassVar[str] = agent_type # Add this for registration _agent_type: ClassVar[str] = agent_type # Add this for registration
@ -115,32 +234,19 @@ class PersonaGenerator(Agent):
system_prompt: str = generate_persona_system_prompt system_prompt: str = generate_persona_system_prompt
age: int = Field(default_factory=lambda: random.randint(22, 67)) age: int = Field(default_factory=lambda: random.randint(22, 67))
gender: str = Field(default_factory=lambda: random.choice(["male", "female"])) 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 = "" username: str = ""
first_name: str = ""
last_name: str = ""
ethnicity: str = ""
generator: Any = Field(default=EthnicNameGenerator(), exclude=True)
llm: Any = Field(default=None, exclude=True) llm: Any = Field(default=None, exclude=True)
model: str = Field(default=None, exclude=True) model: str = Field(default=None, exclude=True)
def randomize(self): def randomize(self):
self.age = random.randint(22, 67) self.age = random.randint(22, 67)
self.gender = random.choice(["male", "female"])
# Use random.choices with explicit type casting to satisfy Literal type # Use random.choices with explicit type casting to satisfy Literal type
self.ethnicity = cast( self.first_name, self.last_name, self.ethnicity, self.gender = self.generator.generate_random_name()
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]
)
async def prepare_message(self, message: Message) -> AsyncGenerator[Message, None]: async def prepare_message(self, message: Message) -> AsyncGenerator[Message, None]:
logger.info(f"{self.agent_type} - {inspect.stack()[0].function}") 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_rag = False
message.tunables.enable_context = False message.tunables.enable_context = False
self.randomize()
message.prompt = f"""\ message.prompt = f"""\
```json ```json
{json.dumps({ {json.dumps({
@ -260,7 +368,10 @@ class PersonaGenerator(Agent):
"full_name": "{persona["full_name"]}", "full_name": "{persona["full_name"]}",
"location": "{persona["location"]}", "location": "{persona["location"]}",
"age": {persona["age"]}, "age": {persona["age"]},
"description": {persona["description"]} "description": {persona["description"]},
"title": {persona["title"]},
"email": {persona["email"]},
"phone": {persona["phone"]}
}} }}
``` ```
""" """