mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 10:15:27 -04:00
Tasks: optional persona for LLM + research tasks (biases output voice)
Wire the existing built-in PERSONAS catalog through to scheduled tasks the same way I wired it to reminder synthesis. Repurposes the dormant scheduled_tasks.character_id column. UI (static/js/tasks.js) - New 'Persona' select in the LLM / Research task form, with the five built-in characters (socrates/razor/nietzsche/spark/odysseus) plus a default 'no persona' option. Pre-populates from existing.character_id on edit. Non-llm/research types explicitly clear it on save. API (routes/task_routes.py) - TaskCreate + TaskUpdate gain character_id: Optional[str]. - _task_to_dict echoes character_id back so the form can hydrate on edit. Update endpoint stores '' as None to allow clearing. Runner (src/task_scheduler.py) - When task.character_id is set and matches a built-in persona, prepend the persona prompt to the task system prompt so the model speaks in that voice while still knowing it's running a scheduled task. - crew_member.personality still wins as the base; character_id stacks on top.
This commit is contained in:
@@ -151,6 +151,7 @@ class TaskCreate(BaseModel):
|
||||
endpoint_url: Optional[str] = None
|
||||
then_task_id: Optional[str] = None # chain: run this task after success
|
||||
notifications_enabled: Optional[bool] = None # None lets action-specific defaults apply
|
||||
character_id: Optional[str] = None # built-in persona id (PERSONAS) — biases output voice
|
||||
|
||||
|
||||
class TaskUpdate(BaseModel):
|
||||
@@ -171,6 +172,7 @@ class TaskUpdate(BaseModel):
|
||||
endpoint_url: Optional[str] = None
|
||||
then_task_id: Optional[str] = None
|
||||
notifications_enabled: Optional[bool] = None
|
||||
character_id: Optional[str] = None
|
||||
|
||||
|
||||
def _display_task_name(t: ScheduledTask) -> str:
|
||||
@@ -203,6 +205,7 @@ def _task_to_dict(t: ScheduledTask, include_last_run_result: bool = False) -> di
|
||||
"output_target": t.output_target,
|
||||
"session_id": t.session_id,
|
||||
"crew_member_id": getattr(t, "crew_member_id", None),
|
||||
"character_id": getattr(t, "character_id", None),
|
||||
"model": t.model,
|
||||
"endpoint_url": t.endpoint_url,
|
||||
"run_count": t.run_count or 0,
|
||||
@@ -552,6 +555,7 @@ def setup_task_routes(task_scheduler) -> APIRouter:
|
||||
then_task_id=then_task_id,
|
||||
webhook_token=webhook_token,
|
||||
notifications_enabled=notifications_enabled,
|
||||
character_id=(req.character_id or None),
|
||||
)
|
||||
db.add(task)
|
||||
db.commit()
|
||||
@@ -705,6 +709,9 @@ def setup_task_routes(task_scheduler) -> APIRouter:
|
||||
task.then_task_id = _validate_then_task_id(db, req.then_task_id, user, current_task_id=task.id)
|
||||
if req.notifications_enabled is not None:
|
||||
task.notifications_enabled = bool(req.notifications_enabled)
|
||||
if req.character_id is not None:
|
||||
# Empty string clears the persona; non-empty stores the id.
|
||||
task.character_id = req.character_id or None
|
||||
if req.cron_expression is not None:
|
||||
if req.cron_expression:
|
||||
try:
|
||||
|
||||
@@ -1335,11 +1335,24 @@ class TaskScheduler:
|
||||
return await self._execute_checkin(task, crew, db, session_id, endpoint_url, model)
|
||||
|
||||
# Build system prompt: crew member persona overrides the default.
|
||||
# Built-in character_id (Socrates, Razor, etc.) further biases the
|
||||
# voice — it prepends to whichever base prompt we landed on so the
|
||||
# task still knows it's executing a scheduled task but in that
|
||||
# character's tone.
|
||||
system_prompt = (
|
||||
(crew.personality or "").strip()
|
||||
if crew and crew.personality
|
||||
else "You are a helpful assistant executing a scheduled task. Use available tools to complete the task thoroughly."
|
||||
)
|
||||
char_id = (getattr(task, "character_id", None) or "").strip()
|
||||
if char_id:
|
||||
try:
|
||||
from src.reminder_personas import PERSONAS as _PERSONAS
|
||||
char_prompt = _PERSONAS.get(char_id.lower())
|
||||
if char_prompt:
|
||||
system_prompt = f"{char_prompt}\n\n{system_prompt}"
|
||||
except Exception:
|
||||
pass
|
||||
# Inject current time so the model knows what's past vs upcoming
|
||||
tz_name = _resolve_task_timezone(db, task)
|
||||
try:
|
||||
|
||||
@@ -1077,9 +1077,23 @@ function _showForm(existing, initTaskType, initTriggerType) {
|
||||
typeOpts.innerHTML = '';
|
||||
if (taskType === 'llm' || taskType === 'research') {
|
||||
const placeholder = taskType === 'research' ? 'What should be researched?' : 'What should the AI do?';
|
||||
const _personaOpts = [
|
||||
['', 'Default (no persona)'],
|
||||
['socrates', 'Socrates'],
|
||||
['razor', 'Razor'],
|
||||
['nietzsche', 'Nietzsche'],
|
||||
['spark', 'Spark'],
|
||||
['odysseus', 'Odysseus'],
|
||||
];
|
||||
const _curPersona = (existing?.character_id || '').toLowerCase();
|
||||
const _personaOptsHtml = _personaOpts.map(([v, label]) =>
|
||||
`<option value="${v}" ${v === _curPersona ? 'selected' : ''}>${label}</option>`).join('');
|
||||
typeOpts.innerHTML = `
|
||||
<label class="task-form-label">${taskType === 'research' ? 'Research question' : 'Prompt'}</label>
|
||||
<textarea id="task-form-prompt" class="task-form-input task-form-textarea" rows="4" placeholder="${placeholder}">${existing?.prompt || ''}</textarea>
|
||||
|
||||
<label class="task-form-label">Persona <span style="opacity:0.5;font-weight:normal;font-size:10px;">(optional — biases the output voice)</span></label>
|
||||
<select id="task-form-persona" class="task-form-input">${_personaOptsHtml}</select>
|
||||
`;
|
||||
} else {
|
||||
typeOpts.innerHTML = `
|
||||
@@ -1437,7 +1451,11 @@ function _showForm(existing, initTaskType, initTriggerType) {
|
||||
return;
|
||||
}
|
||||
payload.prompt = prompt;
|
||||
const personaVal = document.getElementById('task-form-persona')?.value || '';
|
||||
payload.character_id = personaVal;
|
||||
} else {
|
||||
// Non-llm/research tasks: explicitly clear any persona on switch.
|
||||
payload.character_id = '';
|
||||
const action = document.getElementById('task-form-action')?.value;
|
||||
if (!action) {
|
||||
if (uiModule) uiModule.showError('Select an action');
|
||||
|
||||
Reference in New Issue
Block a user