Profile gen working

This commit is contained in:
James Ketr 2025-05-22 16:51:31 -07:00
parent 68712fb418
commit 2355c1dfa5
4 changed files with 173 additions and 73 deletions

View File

@ -42,13 +42,12 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
if (!view) {
return <Box>No user loaded.</Box>;
}
return (
<StyledPaper sx={sx}>
<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}` : ''}
src={view.has_profile ? `/api/u/${view.username}/profile/${sessionId}?timestamp=${Date.now()}` : ''}
alt={`${view.full_name}'s profile`}
sx={{
width: 80,

View File

@ -112,7 +112,8 @@ const streamQueryResponse = (options: StreamQueryOptions) => {
try {
await processLine(line);
} catch (e) {
console.error('Error processing line:', line, e);
console.error('Error processing line:', e);
console.log(line);
}
}
}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState, useRef } from 'react';
import React, { useEffect, useState, useRef, useCallback } from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Tooltip from '@mui/material/Tooltip';
@ -41,32 +41,47 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
const [streaming, setStreaming] = useState<string>('');
const [processing, setProcessing] = useState<boolean>(false);
const [user, setUser] = useState<UserInfo>(emptyUser);
const controllerRef = useRef<StreamQueryController>(null);
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
const promptRef = useRef<string>(null);
const stateRef = useRef<number>(0); /* Generating persona */
const userRef = useRef<UserInfo>(user);
const [userTimestamp, setUserTimestamp] = useState<number>(0);
const [prompt, setPrompt] = useState<string>('');
const [resume, setResume] = useState<string>('');
const [canGenImage, setCanGenImage] = useState<boolean>(false);
const [hasProfile, setHasProfile] = useState<boolean>(false);
const [status, setStatus] = useState<string>('');
const [timestamp, setTimestamp] = useState<number>(0);
const [state, setState] = useState<number>(0); // Replaced stateRef
const [shouldGenerateProfile, setShouldGenerateProfile] = useState<boolean>(false);
// Only keep refs that are truly necessary
const controllerRef = useRef<StreamQueryController>(null);
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
const generateProfile = useCallback((userInfo?: UserInfo, promptText?: string) => {
// Use provided params or current state
const currentUser = userInfo || user;
const currentPrompt = promptText || prompt;
const generateProfile = () => {
if (controllerRef.current) {
return;
}
// Don't generate if we still have blank user data
if (currentUser.username === "[blank]" || currentUser.first_name === "[blank]") {
console.log("Cannot generate profile: user data not ready");
return;
}
const imagePrompt = `A photorealistic profile picture of a ${currentUser.age} year old ${currentUser.gender} ${currentUser.ethnicity} person. ${currentPrompt}`
setStatus(`Generating: ${imagePrompt}`);
setProcessing(true);
setCanGenImage(false);
stateRef.current = 3;
setState(3);
const start = Date.now();
controllerRef.current = streamQueryResponse({
query: {
prompt: `A photorealistic profile picture of a ${userRef.current.age} year old ${userRef.current.gender} ${userRef.current.ethnicity} person. ${prompt}`,
prompt: imagePrompt,
agent_options: {
username: userRef.current.username,
username: currentUser.username,
filename: "profile.png"
}
},
@ -81,20 +96,22 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
if (msg.status === "done") {
setProcessing(false);
controllerRef.current = null;
stateRef.current = 0;
setState(0);
setCanGenImage(true);
setHasProfile(true);
setShouldGenerateProfile(false);
setUser({ ...user, has_profile: true });
}
break;
case "error":
console.log(`Error generating persona: ${msg.response} after ${Date.now() - start}`);
setSnack(msg.response || "", "error");
setProcessing(false);
setUser({...userRef.current});
controllerRef.current = null;
stateRef.current = 0;
setState(0);
setCanGenImage(true);
setHasProfile(true); /* Hack for now */
setShouldGenerateProfile(false);
break;
default:
const data = JSON.parse(msg.response || '');
@ -113,21 +130,20 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
}
}
});
}, [user, prompt, sessionId, setSnack]);
};
const generatePersona = (query: Query) => {
const generatePersona = useCallback((query: Query) => {
if (controllerRef.current) {
return;
}
setPrompt(query.prompt);
promptRef.current = query.prompt;
stateRef.current = 0;
setState(0);
setUser(emptyUser);
setStreaming('');
setResume('');
setProcessing(true);
setCanGenImage(false);
setShouldGenerateProfile(false); // Reset the flag
controllerRef.current = streamQueryResponse({
query,
@ -138,33 +154,35 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
switch (msg.status) {
case "partial":
case "done":
switch (stateRef.current) {
case 0: /* Generating persona */
let partialUser = JSON.parse(jsonrepair((msg.response || '').trim()));
if (!partialUser.full_name) {
partialUser.full_name = `${partialUser.first_name} ${partialUser.last_name}`;
setState(currentState => {
switch (currentState) {
case 0: /* Generating persona */
let partialUser = JSON.parse(jsonrepair((msg.response || '').trim()));
if (!partialUser.full_name) {
partialUser.full_name = `${partialUser.first_name} ${partialUser.last_name}`;
}
console.log("Setting final user data:", partialUser);
setUser({ ...partialUser });
return 1; /* Generating resume */
case 1: /* Generating resume */
setResume(msg.response || '');
return 2; /* RAG generation */
case 2: /* RAG generation */
return 3; /* Image generation */
default:
return currentState;
}
console.log(partialUser);
setUser({...partialUser});
stateRef.current++; /* Generating resume */
break;
case 1: /* Generating resume */
stateRef.current++; /* RAG generation */
setResume(msg.response || '');
break;
case 2: /* RAG generation */
stateRef.current++; /* Image generation */
break;
}
});
if (msg.status === "done") {
setProcessing(false);
setCanGenImage(true);
setStatus('');
controllerRef.current = null;
stateRef.current = 0;
setTimeout(() => {
generateProfile();
}, 0);
setState(0);
// Set flag to trigger profile generation after user state updates
console.log("Persona generation complete, setting shouldGenerateProfile flag");
setShouldGenerateProfile(true);
}
break;
case "thinking":
@ -175,9 +193,9 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
console.log(`Error generating persona: ${msg.response}`);
setSnack(msg.response || "", "error");
setProcessing(false);
setUser({...userRef.current});
setUser(emptyUser);
controllerRef.current = null;
stateRef.current = 0;
setState(0);
break;
}
},
@ -185,32 +203,117 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
setStreaming(chunk);
}
});
};
}, [sessionId, setSnack, generateProfile]);
const cancelQuery = () => {
const cancelQuery = useCallback(() => {
if (controllerRef.current) {
controllerRef.current.abort();
controllerRef.current = null;
stateRef.current = 0;
setState(0);
setProcessing(false);
}
}
}, []);
const onEnter = useCallback((value: string) => {
if (processing) {
return;
}
const query: Query = {
prompt: value,
}
generatePersona(query);
}, [processing, generatePersona]);
const handleSendClick = useCallback(() => {
const value = (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || "";
generatePersona({ prompt: value });
}, [generatePersona]);
// Effect to trigger profile generation when user data is ready
useEffect(() => {
promptRef.current = prompt;
}, [prompt]);
console.log("useEffect triggered - shouldGenerateProfile:", shouldGenerateProfile, "user:", user.username, user.first_name);
if (shouldGenerateProfile && user.username !== "[blank]" && user.first_name !== "[blank]") {
console.log("Triggering profile generation with updated user data:", user);
// Call generateProfile directly with current values to avoid closure issues
if (controllerRef.current) {
console.log("Controller already active, skipping profile generation");
return;
}
useEffect(() => {
userRef.current = user;
}, [user]);
// Don't generate if we still have blank user data
if (user.username === "[blank]" || user.first_name === "[blank]") {
console.log("Cannot generate profile: user data not ready");
return;
}
setProcessing(true);
setCanGenImage(false);
setState(3);
const start = Date.now();
controllerRef.current = streamQueryResponse({
query: {
prompt: `A photorealistic profile picture of a ${user.age} year old ${user.gender} ${user.ethnicity} person. ${prompt}`,
agent_options: {
username: user.username,
filename: "profile.png"
}
},
type: "image",
sessionId,
connectionBase,
onComplete: (msg) => {
console.log("Profile generation response:", msg);
switch (msg.status) {
case "partial":
case "done":
if (msg.status === "done") {
setProcessing(false);
controllerRef.current = null;
setState(0);
setCanGenImage(true);
setHasProfile(true);
setShouldGenerateProfile(false);
}
break;
case "error":
console.log(`Error generating profile: ${msg.response} after ${Date.now() - start}`);
setSnack(msg.response || "", "error");
setProcessing(false);
controllerRef.current = null;
setState(0);
setCanGenImage(true);
setHasProfile(true);
setShouldGenerateProfile(false);
break;
default:
const data = JSON.parse(msg.response || '');
if (msg.status !== "heartbeat") {
console.log(data);
}
if (data.timestamp) {
setTimestamp(data.timestamp);
} else {
setTimestamp(Date.now())
}
if (data.message) {
setStatus(data.message);
}
break;
}
}
});
}
}, [shouldGenerateProfile, user, prompt, sessionId, setSnack]);
// Handle streaming updates based on current state
useEffect(() => {
if (streaming.trim().length === 0) {
return;
}
try {
switch (stateRef.current) {
switch (state) {
case 0: /* Generating persona */
const partialUser = {...emptyUser, ...JSON.parse(jsonrepair(`${streaming.trim()}...`))};
if (!partialUser.full_name) {
@ -227,28 +330,23 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
break;
}
} catch {
// Ignore JSON parsing errors during streaming
}
}, [streaming]);
}, [streaming, state]);
useEffect(() => {
setUserTimestamp(Date.now());
}, [user]);
if (!sessionId) {
return <></>;
}
const onEnter = (value: string) => {
if (processing) {
return;
}
const query: Query = {
prompt: value,
}
generatePersona(query);
};
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}/> }
{user && <CandidateInfo sessionId={sessionId} user={user} />}
{processing &&
<Box sx={{
display: "flex",
@ -269,8 +367,8 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
<Box sx={{display: "flex", flexDirection: "column"}}>
<Box sx={{ display: "flex", flexDirection: "row", position: "relative" }}>
<Avatar
src={hasProfile ? `/api/u/${userRef.current.username}/profile/${sessionId}` : ''}
alt={`${userRef.current.full_name}'s profile`}
src={hasProfile ? `/api/u/${user.username}/profile/${sessionId}` : ''}
alt={`${user.full_name}'s profile`}
sx={{
width: 80,
height: 80,
@ -284,7 +382,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
sx={{ m: 1, gap: 1, flexGrow: 1 }}
variant="contained"
disabled={sessionId === undefined || processing || !canGenImage}
onClick={() => { generateProfile(); }}>
onClick={() => { generateProfile() }}>
Generate Profile Picture<SendIcon />
</Button>
</span>
@ -306,7 +404,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
sx={{ m: 1, gap: 1, flexGrow: 1 }}
variant="contained"
disabled={sessionId === undefined || processing}
onClick={() => { generatePersona({ prompt: (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || "" }); }}>
onClick={handleSendClick}>
Send<SendIcon />
</Button>
</span>
@ -315,7 +413,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
<span style={{ display: "flex" }}> { /* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */}
<IconButton
aria-label="cancel"
onClick={() => { cancelQuery(); }}
onClick={cancelQuery}
sx={{ display: "flex", margin: 'auto 0px' }}
size="large"
edge="start"

View File

@ -47,7 +47,7 @@ emptyUser = {
}
generate_persona_system_prompt = """\
You are a casing director for a movie. Your job is to provide information on ficticious personas for use in a screen play.
You are a casting director for a movie. Your job is to provide information on ficticious personas for use in a screen play.
All response field MUST BE IN ENGLISH, regardless of ethnicity.
@ -80,6 +80,8 @@ Provide all information in English ONLY, with no other commentary:
```
Make sure to provide a username and that the field name for the job description is "description".
DO NOT infer, imply, abbreviate, or state the ethnicity or age in the username or description. You are providing those only for use later by the system when casting individuals for the role.
"""
generate_resume_system_prompt = """
@ -109,7 +111,7 @@ class PersonaGenerator(Agent):
agent_type: Literal["persona"] = "persona" # type: ignore
_agent_type: ClassVar[str] = agent_type # Add this for registration
agent_persist: bool = False
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"]))