human behavior: classifier gate, pacing, splitting, quiet hours, stable prompt prefix
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from io import BytesIO
|
||||
@@ -56,6 +57,29 @@ CONSOLIDATION_RESPONSE_FORMAT = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {"name": "consolidation", "strict": True, "schema": CONSOLIDATION_SCHEMA},
|
||||
}
|
||||
# Reply/ignore + factual pre-pass (SPEC-010 BEH-01): one cheap call
|
||||
CLASSIFIER_SCHEMA = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reply": {"type": "boolean", "description": "Should the assistant answer this message?"},
|
||||
"factual": {"type": "boolean", "description": "Does the user want concrete information (hours, prices, availability)?"},
|
||||
"emoji": {"type": ["string", "null"], "description": "Optional single emoji reaction when not replying, else null."},
|
||||
},
|
||||
"required": ["reply", "factual", "emoji"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
CLASSIFIER_RESPONSE_FORMAT = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {"name": "reply_verdict", "strict": True, "schema": CLASSIFIER_SCHEMA},
|
||||
}
|
||||
CLASSIFIER_SYSTEM = (
|
||||
"You watch a group chat that has an assistant bot. Decide whether the assistant should answer the LAST message:"
|
||||
" reply=true when it addresses the assistant, asks something the assistant can help with, or continues a conversation"
|
||||
" with the assistant; reply=false for human-to-human chatter the assistant should not butt into."
|
||||
" factual=true when the user wants concrete information (opening hours, prices, availability, addresses)."
|
||||
" When reply=false you may suggest one fitting emoji reaction, else null."
|
||||
)
|
||||
|
||||
CONSOLIDATION_SYSTEM = (
|
||||
"You maintain the long-term memory of a Discord assistant. From the observation log, extract NEW durable facts that users stated"
|
||||
" about THEMSELVES only (never record what one user claims about another user), and write one short episode summary of the"
|
||||
@@ -115,6 +139,16 @@ class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
|
||||
logging.warning(f"Failed to generate image {repr(description)}: {repr(err)}")
|
||||
raise RuntimeError(f"Failed to generate image {repr(description)} after multiple retries")
|
||||
|
||||
@staticmethod
|
||||
def _last_author(messages: List[Dict[str, Any]]) -> Optional[str]:
|
||||
try:
|
||||
content = messages[-1]["content"]
|
||||
if not isinstance(content, str):
|
||||
content = content[0]["text"]
|
||||
return str(json.loads(content).get("user")) or None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _record_usage(self, result: Any) -> None:
|
||||
usage = getattr(result, "usage", None)
|
||||
prompt_tokens = getattr(usage, "prompt_tokens", None)
|
||||
@@ -164,6 +198,10 @@ class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
|
||||
"messages": messages,
|
||||
"response_format": ENVELOPE_RESPONSE_FORMAT,
|
||||
}
|
||||
author = self._last_author(messages)
|
||||
if author:
|
||||
# hashed, never the raw Discord name (SAF-10)
|
||||
chat_kwargs["safety_identifier"] = "discord-" + hashlib.sha256(author.encode()).hexdigest()[:16]
|
||||
|
||||
if self.igdb and self.config.get("enable-game-info", False):
|
||||
try:
|
||||
@@ -326,6 +364,25 @@ class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
|
||||
logging.warning(f"failed to translate the text: {repr(err)}")
|
||||
return text
|
||||
|
||||
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():
|
||||
return None
|
||||
tail = "\n".join(str(entry.get("content", ""))[:300] for entry in history_tail[-6:])
|
||||
messages = [
|
||||
{"role": "system", "content": CLASSIFIER_SYSTEM},
|
||||
{"role": "user", "content": f"Recent chat:\n{tail}\n\nLAST message:\n{str(message)}"},
|
||||
]
|
||||
try:
|
||||
result = await openai_chat(
|
||||
self.client, model=self.config["classifier-model"], messages=messages, response_format=CLASSIFIER_RESPONSE_FORMAT
|
||||
)
|
||||
self._record_usage(result)
|
||||
return json.loads(result.choices[0].message.content)
|
||||
except Exception as err:
|
||||
logging.warning(f"classifier failed - failing open: {repr(err)}")
|
||||
return None
|
||||
|
||||
async def consolidate(self, observations: List[Dict[str, Any]], known_facts: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
||||
"""Batched memory consolidation on memory-model (MEM-02)."""
|
||||
if "memory-model" not in self.config or not self.ledger.budget_ok():
|
||||
|
||||
Reference in New Issue
Block a user