Spaces:
Sleeping
Sleeping
NeonClary commited on
Commit ·
4f7950f
1
Parent(s): 1abd9d5
Add AI-completed vs exact role generation style toggle
Browse files- backend/app/api/chat.py +4 -0
- backend/app/services/persona.py +55 -10
- frontend/src/App.js +14 -6
- frontend/src/components/DevMenu.js +16 -0
- frontend/src/utils/api.js +4 -4
backend/app/api/chat.py
CHANGED
|
@@ -28,12 +28,14 @@ class GenerateRoleRequest(BaseModel):
|
|
| 28 |
profile: str = ""
|
| 29 |
identity: str = ""
|
| 30 |
samples: str = ""
|
|
|
|
| 31 |
|
| 32 |
|
| 33 |
class GenerateRoleFreeformRequest(BaseModel):
|
| 34 |
model_id: str
|
| 35 |
name: str = ""
|
| 36 |
text: str = ""
|
|
|
|
| 37 |
|
| 38 |
|
| 39 |
class SetOrchestratorRequest(BaseModel):
|
|
@@ -90,6 +92,7 @@ async def api_generate_role(req: GenerateRoleRequest):
|
|
| 90 |
profile=req.profile,
|
| 91 |
identity=req.identity,
|
| 92 |
samples=req.samples,
|
|
|
|
| 93 |
)
|
| 94 |
if result.get("error"):
|
| 95 |
raise HTTPException(status_code=400, detail=result["error"])
|
|
@@ -102,6 +105,7 @@ async def api_generate_role_freeform(req: GenerateRoleFreeformRequest):
|
|
| 102 |
model_id=req.model_id,
|
| 103 |
name=req.name,
|
| 104 |
text=req.text,
|
|
|
|
| 105 |
)
|
| 106 |
if result.get("error"):
|
| 107 |
raise HTTPException(status_code=400, detail=result["error"])
|
|
|
|
| 28 |
profile: str = ""
|
| 29 |
identity: str = ""
|
| 30 |
samples: str = ""
|
| 31 |
+
role_style: str = "exact"
|
| 32 |
|
| 33 |
|
| 34 |
class GenerateRoleFreeformRequest(BaseModel):
|
| 35 |
model_id: str
|
| 36 |
name: str = ""
|
| 37 |
text: str = ""
|
| 38 |
+
role_style: str = "ai_completed"
|
| 39 |
|
| 40 |
|
| 41 |
class SetOrchestratorRequest(BaseModel):
|
|
|
|
| 92 |
profile=req.profile,
|
| 93 |
identity=req.identity,
|
| 94 |
samples=req.samples,
|
| 95 |
+
role_style=req.role_style,
|
| 96 |
)
|
| 97 |
if result.get("error"):
|
| 98 |
raise HTTPException(status_code=400, detail=result["error"])
|
|
|
|
| 105 |
model_id=req.model_id,
|
| 106 |
name=req.name,
|
| 107 |
text=req.text,
|
| 108 |
+
role_style=req.role_style,
|
| 109 |
)
|
| 110 |
if result.get("error"):
|
| 111 |
raise HTTPException(status_code=400, detail=result["error"])
|
backend/app/services/persona.py
CHANGED
|
@@ -7,20 +7,47 @@ from app.config import settings
|
|
| 7 |
|
| 8 |
LOG = logging.getLogger(__name__)
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
"
|
| 16 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
"The name is: {name}\n"
|
| 18 |
"The identity statement is: {identity}\n"
|
| 19 |
"The profile is: {profile}\n"
|
| 20 |
"Here are the writing and/or speech samples: {samples}"
|
| 21 |
)
|
| 22 |
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
"You will receive freeform information about a character or persona. The input may be "
|
| 25 |
"detailed (with writing samples, background, etc.) or very brief (just a name or a short "
|
| 26 |
"description). Regardless of how much is provided, write a complete, vivid 3-5 sentence "
|
|
@@ -36,6 +63,20 @@ FREEFORM_ROLE_GENERATION_PROMPT = (
|
|
| 36 |
"---\n{text}\n---"
|
| 37 |
)
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
async def _call_llm(model_id: str, prompt_text: str) -> dict:
|
| 41 |
resolved = settings.resolve_model(model_id)
|
|
@@ -70,9 +111,11 @@ async def generate_role_prompt(
|
|
| 70 |
profile: str,
|
| 71 |
identity: str,
|
| 72 |
samples: str,
|
|
|
|
| 73 |
) -> dict:
|
| 74 |
"""Use the selected LLM to distill structured persona inputs into a role prompt."""
|
| 75 |
-
|
|
|
|
| 76 |
name=name or "(not provided)",
|
| 77 |
identity=identity or "(not provided)",
|
| 78 |
profile=profile or "(not provided)",
|
|
@@ -85,9 +128,11 @@ async def generate_role_prompt_freeform(
|
|
| 85 |
model_id: str,
|
| 86 |
name: str,
|
| 87 |
text: str,
|
|
|
|
| 88 |
) -> dict:
|
| 89 |
"""Use the selected LLM to distill a single freeform text block into a role prompt."""
|
| 90 |
-
|
|
|
|
| 91 |
name=name or "(not provided)",
|
| 92 |
text=text or "(not provided)",
|
| 93 |
)
|
|
|
|
| 7 |
|
| 8 |
LOG = logging.getLogger(__name__)
|
| 9 |
|
| 10 |
+
# ---------------------------------------------------------------------------
|
| 11 |
+
# Structured input prompts
|
| 12 |
+
# ---------------------------------------------------------------------------
|
| 13 |
+
|
| 14 |
+
STRUCTURED_AI_COMPLETED_PROMPT = (
|
| 15 |
+
"You will receive structured information about a character or persona: a name, an identity "
|
| 16 |
+
"statement, a profile, and optionally writing/speech samples. Some fields may be sparse or "
|
| 17 |
+
"missing. Write a complete, vivid 3-5 sentence role prompt that an LLM can use to "
|
| 18 |
+
"convincingly embody this persona in a conversation.\n\n"
|
| 19 |
+
"If any fields are sparse, infer plausible personality traits, speech patterns, interests, "
|
| 20 |
+
"and conversational style from whatever clues are available. Fill in realistic detail so "
|
| 21 |
+
"the role prompt is rich and actionable — never produce a vague or skeletal prompt.\n\n"
|
| 22 |
+
"Cover: personality, tone and speech patterns, background/expertise, interests and "
|
| 23 |
+
"motivations, and how they would naturally interact in a casual conversation.\n\n"
|
| 24 |
"The name is: {name}\n"
|
| 25 |
"The identity statement is: {identity}\n"
|
| 26 |
"The profile is: {profile}\n"
|
| 27 |
"Here are the writing and/or speech samples: {samples}"
|
| 28 |
)
|
| 29 |
|
| 30 |
+
STRUCTURED_EXACT_PROMPT = (
|
| 31 |
+
"You will receive structured information about a character or persona: a name, an identity "
|
| 32 |
+
"statement, a profile, and optionally writing/speech samples. Combine this information into "
|
| 33 |
+
"a coherent 3-5 sentence role prompt that an LLM can use to embody this persona in a "
|
| 34 |
+
"conversation.\n\n"
|
| 35 |
+
"IMPORTANT: Use ONLY the information explicitly provided. Do not invent, assume, or infer "
|
| 36 |
+
"any traits, background, opinions, or speech patterns beyond what is stated. Your job is "
|
| 37 |
+
"purely to organize and lightly rephrase the provided facts into a smooth, usable role "
|
| 38 |
+
"prompt — add linking words and natural sentence flow, but no new content. If a field is "
|
| 39 |
+
"empty or says '(not provided)', simply omit it.\n\n"
|
| 40 |
+
"The name is: {name}\n"
|
| 41 |
+
"The identity statement is: {identity}\n"
|
| 42 |
+
"The profile is: {profile}\n"
|
| 43 |
+
"Here are the writing and/or speech samples: {samples}"
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
# ---------------------------------------------------------------------------
|
| 47 |
+
# Freeform input prompts
|
| 48 |
+
# ---------------------------------------------------------------------------
|
| 49 |
+
|
| 50 |
+
FREEFORM_AI_COMPLETED_PROMPT = (
|
| 51 |
"You will receive freeform information about a character or persona. The input may be "
|
| 52 |
"detailed (with writing samples, background, etc.) or very brief (just a name or a short "
|
| 53 |
"description). Regardless of how much is provided, write a complete, vivid 3-5 sentence "
|
|
|
|
| 63 |
"---\n{text}\n---"
|
| 64 |
)
|
| 65 |
|
| 66 |
+
FREEFORM_EXACT_PROMPT = (
|
| 67 |
+
"You will receive freeform information about a character or persona. Combine this "
|
| 68 |
+
"information into a coherent 3-5 sentence role prompt that an LLM can use to embody "
|
| 69 |
+
"this persona in a conversation.\n\n"
|
| 70 |
+
"IMPORTANT: Use ONLY the information explicitly provided. Do not invent, assume, or infer "
|
| 71 |
+
"any traits, background, opinions, or speech patterns beyond what is stated. Your job is "
|
| 72 |
+
"purely to organize and lightly rephrase the user's text into a smooth, usable role "
|
| 73 |
+
"prompt — add linking words and natural sentence flow, but no new content. If very little "
|
| 74 |
+
"was provided, the role prompt should be correspondingly brief.\n\n"
|
| 75 |
+
"The persona's name is: {name}\n\n"
|
| 76 |
+
"Here is everything provided about this persona:\n"
|
| 77 |
+
"---\n{text}\n---"
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
|
| 81 |
async def _call_llm(model_id: str, prompt_text: str) -> dict:
|
| 82 |
resolved = settings.resolve_model(model_id)
|
|
|
|
| 111 |
profile: str,
|
| 112 |
identity: str,
|
| 113 |
samples: str,
|
| 114 |
+
role_style: str = "exact",
|
| 115 |
) -> dict:
|
| 116 |
"""Use the selected LLM to distill structured persona inputs into a role prompt."""
|
| 117 |
+
template = STRUCTURED_AI_COMPLETED_PROMPT if role_style == "ai_completed" else STRUCTURED_EXACT_PROMPT
|
| 118 |
+
prompt_text = template.format(
|
| 119 |
name=name or "(not provided)",
|
| 120 |
identity=identity or "(not provided)",
|
| 121 |
profile=profile or "(not provided)",
|
|
|
|
| 128 |
model_id: str,
|
| 129 |
name: str,
|
| 130 |
text: str,
|
| 131 |
+
role_style: str = "ai_completed",
|
| 132 |
) -> dict:
|
| 133 |
"""Use the selected LLM to distill a single freeform text block into a role prompt."""
|
| 134 |
+
template = FREEFORM_AI_COMPLETED_PROMPT if role_style == "ai_completed" else FREEFORM_EXACT_PROMPT
|
| 135 |
+
prompt_text = template.format(
|
| 136 |
name=name or "(not provided)",
|
| 137 |
text=text or "(not provided)",
|
| 138 |
)
|
frontend/src/App.js
CHANGED
|
@@ -44,6 +44,7 @@ export default function App() {
|
|
| 44 |
const [chatFinished, setChatFinished] = useState(false);
|
| 45 |
const [orchestratorModel, setOrchestratorModel] = useState('');
|
| 46 |
const [personaMode, setPersonaMode] = useState('freeform');
|
|
|
|
| 47 |
const [speedPriority, setSpeedPriorityState] = useState(false);
|
| 48 |
const [auth, setAuth] = useState(null);
|
| 49 |
const [showResponseTime, setShowResponseTime] = useState(false);
|
|
@@ -103,6 +104,11 @@ export default function App() {
|
|
| 103 |
}
|
| 104 |
}, []);
|
| 105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
const handleSpeedPriorityChange = useCallback(async (enabled) => {
|
| 107 |
try {
|
| 108 |
await setSpeedPriority(enabled);
|
|
@@ -177,11 +183,11 @@ export default function App() {
|
|
| 177 |
|
| 178 |
try {
|
| 179 |
const genA = personaMode === 'freeform'
|
| 180 |
-
? generateRoleFreeform({ model_id: selections[0], name: personaA.name, text: personaA.freeform || '' })
|
| 181 |
-
: generateRole({ model_id: selections[0], name: personaA.name, profile: personaA.profile, identity: personaA.identity, samples: personaA.samples });
|
| 182 |
const genB = personaMode === 'freeform'
|
| 183 |
-
? generateRoleFreeform({ model_id: selections[1], name: personaB.name, text: personaB.freeform || '' })
|
| 184 |
-
: generateRole({ model_id: selections[1], name: personaB.name, profile: personaB.profile, identity: personaB.identity, samples: personaB.samples });
|
| 185 |
|
| 186 |
const [roleA, roleB] = await Promise.all([genA, genB]);
|
| 187 |
|
|
@@ -243,7 +249,7 @@ export default function App() {
|
|
| 243 |
abortRef.current = null;
|
| 244 |
getAuthStatus().then(setAuth).catch(() => {});
|
| 245 |
}
|
| 246 |
-
}, [selections, personaA, personaB, personaMode]);
|
| 247 |
|
| 248 |
return (
|
| 249 |
<div className="app">
|
|
@@ -268,7 +274,9 @@ export default function App() {
|
|
| 268 |
orchestratorModel={orchestratorModel}
|
| 269 |
onOrchestratorChange={handleOrchestratorChange}
|
| 270 |
personaMode={personaMode}
|
| 271 |
-
onPersonaModeChange={
|
|
|
|
|
|
|
| 272 |
speedPriority={speedPriority}
|
| 273 |
onSpeedPriorityChange={handleSpeedPriorityChange}
|
| 274 |
showResponseTime={showResponseTime}
|
|
|
|
| 44 |
const [chatFinished, setChatFinished] = useState(false);
|
| 45 |
const [orchestratorModel, setOrchestratorModel] = useState('');
|
| 46 |
const [personaMode, setPersonaMode] = useState('freeform');
|
| 47 |
+
const [roleStyle, setRoleStyle] = useState('ai_completed');
|
| 48 |
const [speedPriority, setSpeedPriorityState] = useState(false);
|
| 49 |
const [auth, setAuth] = useState(null);
|
| 50 |
const [showResponseTime, setShowResponseTime] = useState(false);
|
|
|
|
| 104 |
}
|
| 105 |
}, []);
|
| 106 |
|
| 107 |
+
const handlePersonaModeChange = useCallback((mode) => {
|
| 108 |
+
setPersonaMode(mode);
|
| 109 |
+
setRoleStyle(mode === 'freeform' ? 'ai_completed' : 'exact');
|
| 110 |
+
}, []);
|
| 111 |
+
|
| 112 |
const handleSpeedPriorityChange = useCallback(async (enabled) => {
|
| 113 |
try {
|
| 114 |
await setSpeedPriority(enabled);
|
|
|
|
| 183 |
|
| 184 |
try {
|
| 185 |
const genA = personaMode === 'freeform'
|
| 186 |
+
? generateRoleFreeform({ model_id: selections[0], name: personaA.name, text: personaA.freeform || '', role_style: roleStyle })
|
| 187 |
+
: generateRole({ model_id: selections[0], name: personaA.name, profile: personaA.profile, identity: personaA.identity, samples: personaA.samples, role_style: roleStyle });
|
| 188 |
const genB = personaMode === 'freeform'
|
| 189 |
+
? generateRoleFreeform({ model_id: selections[1], name: personaB.name, text: personaB.freeform || '', role_style: roleStyle })
|
| 190 |
+
: generateRole({ model_id: selections[1], name: personaB.name, profile: personaB.profile, identity: personaB.identity, samples: personaB.samples, role_style: roleStyle });
|
| 191 |
|
| 192 |
const [roleA, roleB] = await Promise.all([genA, genB]);
|
| 193 |
|
|
|
|
| 249 |
abortRef.current = null;
|
| 250 |
getAuthStatus().then(setAuth).catch(() => {});
|
| 251 |
}
|
| 252 |
+
}, [selections, personaA, personaB, personaMode, roleStyle]);
|
| 253 |
|
| 254 |
return (
|
| 255 |
<div className="app">
|
|
|
|
| 274 |
orchestratorModel={orchestratorModel}
|
| 275 |
onOrchestratorChange={handleOrchestratorChange}
|
| 276 |
personaMode={personaMode}
|
| 277 |
+
onPersonaModeChange={handlePersonaModeChange}
|
| 278 |
+
roleStyle={roleStyle}
|
| 279 |
+
onRoleStyleChange={setRoleStyle}
|
| 280 |
speedPriority={speedPriority}
|
| 281 |
onSpeedPriorityChange={handleSpeedPriorityChange}
|
| 282 |
showResponseTime={showResponseTime}
|
frontend/src/components/DevMenu.js
CHANGED
|
@@ -7,6 +7,8 @@ export default function DevMenu({
|
|
| 7 |
onOrchestratorChange,
|
| 8 |
personaMode,
|
| 9 |
onPersonaModeChange,
|
|
|
|
|
|
|
| 10 |
speedPriority,
|
| 11 |
onSpeedPriorityChange,
|
| 12 |
showResponseTime,
|
|
@@ -106,6 +108,20 @@ export default function DevMenu({
|
|
| 106 |
Freeform expert persona input
|
| 107 |
</button>
|
| 108 |
<div className="dev-panel-divider" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
<div className="dev-panel-label">Display options</div>
|
| 110 |
<button onClick={() => onShowResponseTimeChange(!showResponseTime)}>
|
| 111 |
{showResponseTime ? <Eye size={14} /> : <EyeOff size={14} />}
|
|
|
|
| 7 |
onOrchestratorChange,
|
| 8 |
personaMode,
|
| 9 |
onPersonaModeChange,
|
| 10 |
+
roleStyle,
|
| 11 |
+
onRoleStyleChange,
|
| 12 |
speedPriority,
|
| 13 |
onSpeedPriorityChange,
|
| 14 |
showResponseTime,
|
|
|
|
| 108 |
Freeform expert persona input
|
| 109 |
</button>
|
| 110 |
<div className="dev-panel-divider" />
|
| 111 |
+
<div className="dev-panel-label">Role generation</div>
|
| 112 |
+
<button
|
| 113 |
+
disabled={roleStyle === 'ai_completed'}
|
| 114 |
+
onClick={() => onRoleStyleChange('ai_completed')}
|
| 115 |
+
>
|
| 116 |
+
AI completed roles
|
| 117 |
+
</button>
|
| 118 |
+
<button
|
| 119 |
+
disabled={roleStyle === 'exact'}
|
| 120 |
+
onClick={() => onRoleStyleChange('exact')}
|
| 121 |
+
>
|
| 122 |
+
Exact user roles
|
| 123 |
+
</button>
|
| 124 |
+
<div className="dev-panel-divider" />
|
| 125 |
<div className="dev-panel-label">Display options</div>
|
| 126 |
<button onClick={() => onShowResponseTimeChange(!showResponseTime)}>
|
| 127 |
{showResponseTime ? <Eye size={14} /> : <EyeOff size={14} />}
|
frontend/src/utils/api.js
CHANGED
|
@@ -8,11 +8,11 @@ export async function fetchModels() {
|
|
| 8 |
return resp.json();
|
| 9 |
}
|
| 10 |
|
| 11 |
-
export async function generateRole({ model_id, name, profile, identity, samples }) {
|
| 12 |
const resp = await fetch(`${API_BASE}/api/chat/generate-role`, {
|
| 13 |
method: 'POST',
|
| 14 |
headers: { 'Content-Type': 'application/json' },
|
| 15 |
-
body: JSON.stringify({ model_id, name, profile, identity, samples }),
|
| 16 |
});
|
| 17 |
if (!resp.ok) {
|
| 18 |
const err = await resp.json().catch(() => ({ detail: resp.statusText }));
|
|
@@ -21,11 +21,11 @@ export async function generateRole({ model_id, name, profile, identity, samples
|
|
| 21 |
return resp.json();
|
| 22 |
}
|
| 23 |
|
| 24 |
-
export async function generateRoleFreeform({ model_id, name, text }) {
|
| 25 |
const resp = await fetch(`${API_BASE}/api/chat/generate-role-freeform`, {
|
| 26 |
method: 'POST',
|
| 27 |
headers: { 'Content-Type': 'application/json' },
|
| 28 |
-
body: JSON.stringify({ model_id, name, text }),
|
| 29 |
});
|
| 30 |
if (!resp.ok) {
|
| 31 |
const err = await resp.json().catch(() => ({ detail: resp.statusText }));
|
|
|
|
| 8 |
return resp.json();
|
| 9 |
}
|
| 10 |
|
| 11 |
+
export async function generateRole({ model_id, name, profile, identity, samples, role_style }) {
|
| 12 |
const resp = await fetch(`${API_BASE}/api/chat/generate-role`, {
|
| 13 |
method: 'POST',
|
| 14 |
headers: { 'Content-Type': 'application/json' },
|
| 15 |
+
body: JSON.stringify({ model_id, name, profile, identity, samples, role_style }),
|
| 16 |
});
|
| 17 |
if (!resp.ok) {
|
| 18 |
const err = await resp.json().catch(() => ({ detail: resp.statusText }));
|
|
|
|
| 21 |
return resp.json();
|
| 22 |
}
|
| 23 |
|
| 24 |
+
export async function generateRoleFreeform({ model_id, name, text, role_style }) {
|
| 25 |
const resp = await fetch(`${API_BASE}/api/chat/generate-role-freeform`, {
|
| 26 |
method: 'POST',
|
| 27 |
headers: { 'Content-Type': 'application/json' },
|
| 28 |
+
body: JSON.stringify({ model_id, name, text, role_style }),
|
| 29 |
});
|
| 30 |
if (!resp.ok) {
|
| 31 |
const err = await resp.json().catch(() => ({ detail: resp.statusText }));
|