fix defects D1-D12 batch 1, structured envelope, saf gates + ops kill-switches

This commit is contained in:
Oleksandr Kozachuk
2026-07-13 12:56:32 +02:00
parent f1578cbd99
commit 6e5abf3d2c
14 changed files with 770 additions and 214 deletions
+72 -87
View File
@@ -1,3 +1,4 @@
import asyncio
import json
import logging
import os
@@ -11,8 +12,6 @@ from pathlib import Path
from pprint import pformat
from typing import Any, Dict, List, Optional, Tuple, Union
import multiline
def pp(*args, **kw):
if "width" not in kw:
@@ -22,14 +21,16 @@ def pp(*args, **kw):
@lru_cache(maxsize=300)
def parse_json(content: str) -> Dict:
content = content.strip()
try:
return json.loads(content)
except Exception:
try:
return multiline.loads(content, multiline=True)
except Exception as err:
raise err
# Strict JSON only — model output is schema-enforced (ENV-18/19),
# history entries are json.dumps products.
return json.loads(content.strip())
def sanitize_external_text(text: str, max_len: int = 4000) -> str:
"""Neutralize attacker-influenced text before it enters a prompt (SAF-03)."""
text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", "", text)
text = text.replace("@everyone", "@everyone").replace("@here", "@here")
return text[:max_len]
def exponential_backoff(base=2, max_delay=60, factor=1, jitter=0.1, max_attempts=None):
@@ -84,30 +85,6 @@ def async_cache_to_file(filename):
return decorator
def parse_maybe_json(json_string):
if json_string is None:
return None
if isinstance(json_string, (list, dict)):
return " ".join(map(str, (json_string.values() if isinstance(json_string, dict) else json_string)))
json_string = str(json_string).strip()
try:
parsed_json = parse_json(json_string)
except Exception:
for b, e in [("{", "}"), ("[", "]")]:
if json_string.startswith(b) and json_string.endswith(e):
return parse_maybe_json(json_string[1:-1])
return json_string
if isinstance(parsed_json, str):
return parsed_json
if isinstance(parsed_json, (list, dict)):
return "\n".join(map(str, (parsed_json.values() if isinstance(parsed_json, dict) else parsed_json)))
return str(parsed_json)
def same_channel(item1: Dict[str, Any], item2: Dict[str, Any]) -> bool:
return parse_json(item1["content"]).get("channel") == parse_json(item2["content"]).get("channel")
class AIMessageBase(object):
def __init__(self) -> None:
self.vars: List[str] = []
@@ -182,7 +159,7 @@ class AIResponder(AIResponderBase):
if news_feed and os.path.exists(news_feed):
with open(news_feed) as fd:
news_feed = fd.read().strip()
system = system.replace("{news}", news_feed)
system = system.replace("{news}", sanitize_external_text(news_feed))
system = system.replace("{memory}", self.memory)
messages.append({"role": "system", "content": system})
if limit is not None:
@@ -211,30 +188,26 @@ class AIResponder(AIResponderBase):
raise NotImplementedError()
async def post_process(self, message: AIMessage, response: Dict[str, Any]) -> AIResponse:
for fld in ("answer", "channel", "staff", "picture", "hack"):
if str(response.get(fld)).strip().lower() in ("none", "", "null", '"none"', '"null"', "'none'", "'null'"):
response[fld] = None
for fld in ("answer_needed", "hack", "picture_edit"):
if str(response.get(fld)).strip().lower() == "true":
response[fld] = True
else:
response[fld] = False
if response["answer"] is None:
response["answer_needed"] = False
# Envelope arrives schema-validated (ENV-19); .get defaults keep old
# history entries and hand-built test dicts working.
answer = response.get("answer")
answer_needed = bool(response.get("answer_needed", False))
if answer is None:
answer_needed = False
else:
response["answer"] = str(response["answer"])
response["answer"] = re.sub(r"@\[([^\]]*)\]\([^\)]*\)", r"\1", response["answer"])
response["answer"] = re.sub(r"\[[^\]]*\]\(([^\)]*)\)", r"\1", response["answer"])
answer = str(answer)
answer = re.sub(r"@\[([^\]]*)\]\([^\)]*\)", r"\1", answer)
answer = re.sub(r"\[[^\]]*\]\(([^\)]*)\)", r"\1", answer)
if message.direct or message.user in message.message:
response["answer_needed"] = True
answer_needed = True
response_message = AIResponse(
response["answer"],
response["answer_needed"],
parse_maybe_json(response["channel"]),
parse_maybe_json(response["staff"]),
parse_maybe_json(response["picture"]),
response["picture_edit"],
response["hack"],
answer,
answer_needed,
response.get("channel"),
response.get("staff"),
response.get("picture"),
bool(response.get("picture_edit", False)),
bool(response.get("hack", False)),
)
if response_message.staff is not None and response_message.answer is not None:
response_message.answer_needed = True
@@ -261,25 +234,32 @@ class AIResponder(AIResponderBase):
async def chat(self, messages: List[Dict[str, Any]], limit: int) -> Tuple[Optional[Dict[str, Any]], int]:
raise NotImplementedError()
async def fix(self, answer: str) -> str:
raise NotImplementedError()
async def memory_rewrite(self, memory: str, message_user: str, answer_user: str, question: str, answer: str) -> str:
raise NotImplementedError()
async def translate(self, text: str, language: str = "english") -> str:
raise NotImplementedError()
def shrink_history_by_one(self, index: int = 0) -> None:
if index >= len(self.history):
del self.history[0]
else:
current = self.history[index]
count = sum(1 for item in self.history if same_channel(item, current))
if count > self.config.get("history-per-channel", 3):
@staticmethod
def _entry_channel(item: Dict[str, Any]) -> Optional[str]:
try:
return parse_json(item["content"]).get("channel")
except Exception:
return None
def shrink_history_by_one(self) -> None:
if not self.history:
return
cap = self.config.get("history-per-channel", 3)
counts: Dict[Optional[str], int] = {}
for item in self.history:
chan = self._entry_channel(item)
counts[chan] = counts.get(chan, 0) + 1
for index, item in enumerate(self.history):
if counts[self._entry_channel(item)] > cap:
del self.history[index]
else:
self.shrink_history_by_one(index + 1)
return
del self.history[0]
def update_history(self, question: Dict[str, Any], answer: Dict[str, Any], limit: int, historise_question: bool = True) -> None:
if not isinstance(question["content"], str):
@@ -294,6 +274,7 @@ class AIResponder(AIResponderBase):
pickle.dump(self.history, fd)
def update_memory(self, memory) -> None:
self.memory = memory
if self.memory_file is not None:
with open(self.memory_file, "wb") as fd:
pickle.dump(self.memory, fd)
@@ -306,6 +287,15 @@ class AIResponder(AIResponderBase):
response["picture"] = await self.translate(response["picture"])
return True
def _parse_answer(self, answer: Dict[str, Any]) -> Optional[Dict[str, Any]]:
# Schema-enforced output should always parse; anything else is a
# failed attempt — no repair model (ENV-18).
try:
return parse_json(answer["content"])
except Exception as err:
logging.error(f"failed to parse the answer: {pp(err)}\n{repr(answer['content'])}")
return None
async def memoize(self, message_user: str, answer_user: str, message: str, answer: str) -> None:
self.memory = await self.memory_rewrite(self.memory, message_user, answer_user, message, answer)
self.update_memory(self.memory)
@@ -324,8 +314,16 @@ class AIResponder(AIResponderBase):
if self.short_path(message, limit):
return AIResponse(None, False, None, None, None, False, False)
# Number of retries for sending the message
# Number of retries for sending the message; failed attempts are
# spaced by exponential backoff (ENV-12 / D1)
retries = 3
backoff = exponential_backoff(max_delay=10)
async def failed_attempt() -> None:
nonlocal retries
retries -= 1
if retries > 0:
await asyncio.sleep(next(backoff))
while retries > 0:
# Get the message queue
@@ -336,26 +334,13 @@ class AIResponder(AIResponderBase):
answer, limit = await self.chat(messages, limit)
if answer is None:
retries -= 1
await failed_attempt()
continue
# Attempt to parse the AI's response
try:
response = parse_json(answer["content"])
except Exception as err:
logging.warning(f"failed to parse the answer: {pp(err)}\n{repr(answer['content'])}")
answer["content"] = await self.fix(answer["content"])
# Retry parsing the fixed content
try:
response = parse_json(answer["content"])
except Exception as err:
logging.error(f"failed to parse the fixed answer: {pp(err)}\n{repr(answer['content'])}")
retries -= 1
continue
if not await self.handle_picture(response):
retries -= 1
# Attempt to parse the AI's response (strict — ENV-18)
response = self._parse_answer(answer)
if response is None or not await self.handle_picture(response):
await failed_attempt()
continue
# Post-process the message and update the answer's content