Files
mem0/integrations/codex-plugin/core/message_utils.py

128 lines
4.5 KiB
Python

"""Shared, host-independent redaction and lossless extraction batching."""
from __future__ import annotations
import json
import math
import re
from typing import Any
MAX_EXTRACTION_INPUT_TOKENS = 24000
SECRET_PATTERNS = [
re.compile(r"(?i)(authorization\s*[:=]\s*(?:bearer|token)\s+)[^\s\"']+"),
re.compile(r"(?i)((?:api[_-]?key|secret[_-]?access[_-]?key|session[_-]?token)\s*[:=]\s*)[^\s\"']+"),
re.compile(
r"(?i)((?:access[_-]?token|refresh[_-]?token|password|credential)"
r"\s*[:=]\s*)[^\s&\"']+"
),
re.compile(r"\b(?:sk|m0|mem0_sk|psk)-[A-Za-z0-9_\-]{12,}\b"),
re.compile(r"\b(?:ASIA|AKIA)[A-Z0-9]{12,}\b"),
re.compile(r"\b(?:ghp_|github_pat_|xox[baprs]-)[A-Za-z0-9_\-]{12,}\b"),
re.compile(
r"-----BEGIN [^-]*PRIVATE KEY-----.*?-----END [^-]*PRIVATE KEY-----",
re.DOTALL,
),
re.compile(
r'(?i)("(?:api[_-]?key|password|secret(?:[_-]?access[_-]?key)?'
r"|(?:access|refresh|session)[_-]?token|token|authorization|credential"
r')"\s*:\s*")(?:\\.|[^"\\])*'
),
]
def redact(value: Any) -> str:
text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, default=str)
for pattern in SECRET_PATTERNS:
if pattern.groups:
text = pattern.sub(r"\1[REDACTED]", text)
else:
text = pattern.sub("[REDACTED]", text)
return text
def _estimated_tokens(value: str) -> int:
"""Conservatively estimate tokens without adding a tokenizer dependency."""
ascii_chars = sum(ord(char) < 128 for char in value)
return math.ceil((ascii_chars * 0.4) + (len(value) - ascii_chars))
def _message_tokens(messages: list[dict[str, str]]) -> int:
return _estimated_tokens(json.dumps(messages, ensure_ascii=False))
def _is_agent_assignment(message: dict[str, str]) -> bool:
return message.get("role") == "assistant" and message.get("content", "").startswith("Subagent assignment (")
def _is_agent_response(message: dict[str, str]) -> bool:
return message.get("role") == "assistant" and message.get("content", "").startswith("Subagent response (")
def extraction_message_batches(
messages: list[dict[str, str]],
*,
max_tokens: int = MAX_EXTRACTION_INPUT_TOKENS,
) -> list[list[dict[str, str]]]:
"""Keep exchanges together when possible; split oversized messages to enforce the request budget."""
if not messages or _message_tokens(messages) <= max_tokens:
return [messages]
exchanges: list[list[dict[str, str]]] = []
exchange: list[dict[str, str]] = []
for message in messages:
if message.get("role") == "user" and exchange:
exchanges.append(exchange)
exchange = []
exchange.append(message)
if exchange:
exchanges.append(exchange)
units: list[list[dict[str, str]]] = []
for exchange in exchanges:
if _message_tokens(exchange) <= max_tokens:
units.append(exchange)
continue
index = 0
while index < len(exchange):
message = exchange[index]
if _is_agent_assignment(message) and index + 1 < len(exchange) and _is_agent_response(exchange[index + 1]):
units.append(exchange[index : index + 2])
index += 2
else:
units.append([message])
index += 1
bounded_units: list[list[dict[str, str]]] = []
for unit in units:
if _message_tokens(unit) <= max_tokens:
bounded_units.append(unit)
continue
for message in unit:
remaining = message["content"]
while remaining:
low, high = 0, len(remaining)
while low < high:
middle = (low + high + 1) // 2
if _message_tokens([{**message, "content": remaining[:middle]}]) <= max_tokens:
low = middle
else:
high = middle - 1
if low == 0:
raise ValueError("Extraction token budget cannot fit a message")
bounded_units.append([{**message, "content": remaining[:low]}])
remaining = remaining[low:]
batches: list[list[dict[str, str]]] = []
batch: list[dict[str, str]] = []
for unit in bounded_units:
candidate = [*batch, *unit]
if batch and _message_tokens(candidate) > max_tokens:
batches.append(batch)
batch = list(unit)
else:
batch = candidate
if batch:
batches.append(batch)
return batches