From 2355c1dfa52c656fcd8fdec05e814eca078430e3 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Thu, 22 May 2025 16:51:31 -0700 Subject: [PATCH] Profile gen working --- .../src/NewApp/Components/CandidateInfo.tsx | 3 +- .../NewApp/Components/streamQueryResponse.tsx | 3 +- .../src/NewApp/Pages/GenerateCandidate.tsx | 234 +++++++++++++----- src/utils/agents/persona_generator.py | 6 +- 4 files changed, 173 insertions(+), 73 deletions(-) diff --git a/frontend/src/NewApp/Components/CandidateInfo.tsx b/frontend/src/NewApp/Components/CandidateInfo.tsx index dbf46b0..8d014ce 100644 --- a/frontend/src/NewApp/Components/CandidateInfo.tsx +++ b/frontend/src/NewApp/Components/CandidateInfo.tsx @@ -42,13 +42,12 @@ const CandidateInfo: React.FC = (props: CandidateInfoProps) if (!view) { return No user loaded.; } - return ( { try { await processLine(line); } catch (e) { - console.error('Error processing line:', line, e); + console.error('Error processing line:', e); + console.log(line); } } } diff --git a/frontend/src/NewApp/Pages/GenerateCandidate.tsx b/frontend/src/NewApp/Pages/GenerateCandidate.tsx index 5286922..347c4b4 100644 --- a/frontend/src/NewApp/Pages/GenerateCandidate.tsx +++ b/frontend/src/NewApp/Pages/GenerateCandidate.tsx @@ -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(''); const [processing, setProcessing] = useState(false); const [user, setUser] = useState(emptyUser); - const controllerRef = useRef(null); - const backstoryTextRef = useRef(null); - const promptRef = useRef(null); - const stateRef = useRef(0); /* Generating persona */ - const userRef = useRef(user); + const [userTimestamp, setUserTimestamp] = useState(0); const [prompt, setPrompt] = useState(''); const [resume, setResume] = useState(''); const [canGenImage, setCanGenImage] = useState(false); const [hasProfile, setHasProfile] = useState(false); const [status, setStatus] = useState(''); const [timestamp, setTimestamp] = useState(0); + const [state, setState] = useState(0); // Replaced stateRef + const [shouldGenerateProfile, setShouldGenerateProfile] = useState(false); + + // Only keep refs that are truly necessary + const controllerRef = useRef(null); + const backstoryTextRef = useRef(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 ( - { user && } + {user && } {processing && { { sx={{ m: 1, gap: 1, flexGrow: 1 }} variant="contained" disabled={sessionId === undefined || processing || !canGenImage} - onClick={() => { generateProfile(); }}> + onClick={() => { generateProfile() }}> Generate Profile Picture @@ -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 @@ -315,7 +413,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => { { /* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */} { cancelQuery(); }} + onClick={cancelQuery} sx={{ display: "flex", margin: 'auto 0px' }} size="large" edge="start" diff --git a/src/utils/agents/persona_generator.py b/src/utils/agents/persona_generator.py index d2a9151..f0b74c9 100644 --- a/src/utils/agents/persona_generator.py +++ b/src/utils/agents/persona_generator.py @@ -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"]))