self-tasking engine: persistent queue, idle-impulse + follow-up generators, approval mode
This commit is contained in:
@@ -58,6 +58,31 @@ CONSOLIDATION_RESPONSE_FORMAT = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {"name": "consolidation", "strict": True, "schema": CONSOLIDATION_SCHEMA},
|
||||
}
|
||||
# Follow-up task proposal (SPEC-005 TSK-08): one task or null
|
||||
TASKGEN_SCHEMA = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task": {
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"channel": {"type": ["string", "null"], "description": "Target channel, or null for the main chat channel."},
|
||||
"prompt": {"type": "string", "description": "Instruction the assistant will act on when the task runs."},
|
||||
"due_hours": {"type": "number", "description": "Hours from now until the task should run (0 = now)."},
|
||||
},
|
||||
"required": ["channel", "prompt", "due_hours"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
},
|
||||
"required": ["task"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
TASKGEN_RESPONSE_FORMAT = {"type": "json_schema", "json_schema": {"name": "task_proposal", "strict": True, "schema": TASKGEN_SCHEMA}}
|
||||
TASKGEN_SYSTEM = (
|
||||
"You plan the self-initiated actions of a Discord assistant. Given recent conversation summaries, propose AT MOST ONE follow-up"
|
||||
" worth doing on the assistant's own initiative (ask how something announced went, revisit an open question, congratulate on an"
|
||||
" event). Only propose something genuinely worthwhile — when in doubt, return a null task."
|
||||
)
|
||||
|
||||
# Reply/ignore + factual pre-pass (SPEC-010 BEH-01): one cheap call
|
||||
CLASSIFIER_SCHEMA = {
|
||||
"type": "object",
|
||||
@@ -381,6 +406,29 @@ class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
|
||||
logging.info(f"edited {len(buffers)} image(s) on {model} from {len(handles)} input(s)")
|
||||
return buffers
|
||||
|
||||
async def propose_task(self) -> Optional[Dict[str, Any]]:
|
||||
"""One follow-up proposal from recent episodes on memory-model (TSK-08)."""
|
||||
if "memory-model" not in self.config or self.store is None or not self.ledger.budget_ok():
|
||||
return None
|
||||
channel = self.config.get("chat-channel", "chat")
|
||||
episodes = await asyncio.to_thread(self.store.recent_episodes, channel, 5)
|
||||
if not episodes:
|
||||
return None
|
||||
episode_lines = "\n".join(f"- {episode}" for episode in episodes)
|
||||
messages = [
|
||||
{"role": "system", "content": TASKGEN_SYSTEM},
|
||||
{"role": "user", "content": f"Recent conversation summaries in #{channel}:\n{episode_lines}"},
|
||||
]
|
||||
try:
|
||||
result = await openai_chat(
|
||||
self.client, model=self.config["memory-model"], messages=messages, response_format=TASKGEN_RESPONSE_FORMAT
|
||||
)
|
||||
self._record_usage(result)
|
||||
return json.loads(result.choices[0].message.content)
|
||||
except Exception as err:
|
||||
logging.warning(f"task proposal failed: {repr(err)}")
|
||||
return None
|
||||
|
||||
async def classify(self, message: Any, history_tail: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
||||
"""~100-token reply/factual/emoji verdict on classifier-model (BEH-01/03)."""
|
||||
if "classifier-model" not in self.config or not self.ledger.budget_ok():
|
||||
|
||||
Reference in New Issue
Block a user