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 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
- ROLE_GENERATION_PROMPT = (
11
- 'Write a concise but highly informative 3-5 sentence prompt which when used as the '
12
- '"role" input will enable an LLM to respond in a way that sounds like the authentic '
13
- "voice of the person/character whose writing and/or speaking samples are provided here. "
14
- "Consider personality, speech patterns, identity, interests, motivation, and "
15
- "conversational style. Integrate the identity statement, profile information and name "
16
- "provided.\n"
 
 
 
 
 
 
 
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
- FREEFORM_ROLE_GENERATION_PROMPT = (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- prompt_text = ROLE_GENERATION_PROMPT.format(
 
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
- prompt_text = FREEFORM_ROLE_GENERATION_PROMPT.format(
 
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={setPersonaMode}
 
 
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 }));