From 2bf372b41c9083b255d2abd880b89adadd192972 Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Wed, 10 Jun 2026 23:36:18 +0900 Subject: [PATCH] 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. --- routes/task_routes.py | 7 +++++++ src/task_scheduler.py | 13 +++++++++++++ static/js/tasks.js | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/routes/task_routes.py b/routes/task_routes.py index 5734fcb22..5698353bf 100644 --- a/routes/task_routes.py +++ b/routes/task_routes.py @@ -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: diff --git a/src/task_scheduler.py b/src/task_scheduler.py index 999a0699d..eb4617531 100644 --- a/src/task_scheduler.py +++ b/src/task_scheduler.py @@ -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: diff --git a/static/js/tasks.js b/static/js/tasks.js index 8437b4250..a74bde151 100644 --- a/static/js/tasks.js +++ b/static/js/tasks.js @@ -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]) => + ``).join(''); typeOpts.innerHTML = ` + + + `; } 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');