@miller-tech/uap 1.20.13 → 1.20.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -219,7 +219,7 @@ PROXY_MALFORMED_TOOL_GUARDRAIL = os.environ.get(
|
|
|
219
219
|
"no",
|
|
220
220
|
}
|
|
221
221
|
PROXY_MALFORMED_TOOL_RETRY_MAX = int(
|
|
222
|
-
os.environ.get("PROXY_MALFORMED_TOOL_RETRY_MAX", "
|
|
222
|
+
os.environ.get("PROXY_MALFORMED_TOOL_RETRY_MAX", "3")
|
|
223
223
|
)
|
|
224
224
|
PROXY_MALFORMED_TOOL_RETRY_MAX_TOKENS = int(
|
|
225
225
|
os.environ.get("PROXY_MALFORMED_TOOL_RETRY_MAX_TOKENS", "2048")
|
|
@@ -3890,6 +3890,40 @@ async def _apply_completion_contract_guardrail(
|
|
|
3890
3890
|
return retried
|
|
3891
3891
|
|
|
3892
3892
|
|
|
3893
|
+
def _sanitize_assistant_messages_for_retry(messages: list[dict]) -> list[dict]:
|
|
3894
|
+
"""Strip malformed tool-like text from assistant messages to prevent copy-contamination.
|
|
3895
|
+
|
|
3896
|
+
Only sanitizes the last 4 assistant messages to avoid excessive processing.
|
|
3897
|
+
"""
|
|
3898
|
+
import re
|
|
3899
|
+
|
|
3900
|
+
# Patterns that indicate malformed tool call text in assistant content
|
|
3901
|
+
_TOOL_LIKE_PATTERNS = re.compile(
|
|
3902
|
+
r"<tool_call>.*?</tool_call>"
|
|
3903
|
+
r"|<function_call>.*?</function_call>"
|
|
3904
|
+
r'|\{"name"\s*:\s*"[^"]+"\s*,\s*"arguments"\s*:'
|
|
3905
|
+
r"|```json\s*\{[^}]*\"name\"\s*:",
|
|
3906
|
+
re.DOTALL,
|
|
3907
|
+
)
|
|
3908
|
+
|
|
3909
|
+
result = list(messages)
|
|
3910
|
+
sanitized_count = 0
|
|
3911
|
+
for i in range(len(result) - 1, -1, -1):
|
|
3912
|
+
if sanitized_count >= 4:
|
|
3913
|
+
break
|
|
3914
|
+
msg = result[i]
|
|
3915
|
+
if msg.get("role") != "assistant":
|
|
3916
|
+
continue
|
|
3917
|
+
content = msg.get("content", "")
|
|
3918
|
+
if isinstance(content, str) and _TOOL_LIKE_PATTERNS.search(content):
|
|
3919
|
+
cleaned = _TOOL_LIKE_PATTERNS.sub("", content).strip()
|
|
3920
|
+
if not cleaned:
|
|
3921
|
+
cleaned = "I will use the appropriate tool."
|
|
3922
|
+
result[i] = {**msg, "content": cleaned}
|
|
3923
|
+
sanitized_count += 1
|
|
3924
|
+
return result
|
|
3925
|
+
|
|
3926
|
+
|
|
3893
3927
|
def _build_malformed_retry_body(
|
|
3894
3928
|
openai_body: dict,
|
|
3895
3929
|
anthropic_body: dict,
|
|
@@ -3901,7 +3935,11 @@ def _build_malformed_retry_body(
|
|
|
3901
3935
|
retry_body = dict(openai_body)
|
|
3902
3936
|
retry_body["stream"] = False
|
|
3903
3937
|
retry_body["tool_choice"] = tool_choice
|
|
3904
|
-
|
|
3938
|
+
# Escalate temperature down on successive retries for more deterministic output
|
|
3939
|
+
if total_attempts > 1 and attempt > 1:
|
|
3940
|
+
retry_body["temperature"] = 0.0
|
|
3941
|
+
else:
|
|
3942
|
+
retry_body["temperature"] = PROXY_MALFORMED_TOOL_RETRY_TEMPERATURE
|
|
3905
3943
|
|
|
3906
3944
|
if tool_choice == "required":
|
|
3907
3945
|
retry_instruction = (
|
|
@@ -3922,7 +3960,10 @@ def _build_malformed_retry_body(
|
|
|
3922
3960
|
}
|
|
3923
3961
|
existing_messages = retry_body.get("messages")
|
|
3924
3962
|
if isinstance(existing_messages, list) and existing_messages:
|
|
3925
|
-
|
|
3963
|
+
# Strip malformed tool-like text from assistant messages to prevent
|
|
3964
|
+
# the model from copying contaminated patterns on retry
|
|
3965
|
+
sanitized = _sanitize_assistant_messages_for_retry(existing_messages)
|
|
3966
|
+
retry_body["messages"] = [*sanitized, malformed_retry_instruction]
|
|
3926
3967
|
|
|
3927
3968
|
if PROXY_MALFORMED_TOOL_RETRY_MAX_TOKENS > 0:
|
|
3928
3969
|
current_max = int(
|
|
@@ -3377,6 +3377,66 @@ class TestCycleBreakOptions(unittest.TestCase):
|
|
|
3377
3377
|
self.assertEqual(monitor.cycling_tool_names, [])
|
|
3378
3378
|
|
|
3379
3379
|
|
|
3380
|
+
class TestMalformedRetryHardening(unittest.TestCase):
|
|
3381
|
+
"""Tests for malformed retry improvements: budget, temp escalation, message sanitization."""
|
|
3382
|
+
|
|
3383
|
+
def test_retry_max_default_is_3(self):
|
|
3384
|
+
"""Option 1: default retry budget increased from 2 to 3."""
|
|
3385
|
+
self.assertEqual(proxy.PROXY_MALFORMED_TOOL_RETRY_MAX, 3)
|
|
3386
|
+
|
|
3387
|
+
def test_sanitize_assistant_messages_strips_tool_like_text(self):
|
|
3388
|
+
"""Option 3: malformed tool-like text stripped from assistant messages on retry."""
|
|
3389
|
+
messages = [
|
|
3390
|
+
{"role": "system", "content": "You are helpful."},
|
|
3391
|
+
{"role": "user", "content": "Run a command"},
|
|
3392
|
+
{"role": "assistant", "content": 'Here is the result <tool_call>{"name": "Bash", "arguments": {"command": "ls"}}</tool_call>'},
|
|
3393
|
+
{"role": "user", "content": "ok"},
|
|
3394
|
+
]
|
|
3395
|
+
sanitized = proxy._sanitize_assistant_messages_for_retry(messages)
|
|
3396
|
+
# System and user messages unchanged
|
|
3397
|
+
self.assertEqual(sanitized[0]["content"], "You are helpful.")
|
|
3398
|
+
self.assertEqual(sanitized[1]["content"], "Run a command")
|
|
3399
|
+
self.assertEqual(sanitized[3]["content"], "ok")
|
|
3400
|
+
# Assistant message should have tool_call stripped
|
|
3401
|
+
self.assertNotIn("<tool_call>", sanitized[2]["content"])
|
|
3402
|
+
self.assertNotIn("Bash", sanitized[2]["content"])
|
|
3403
|
+
|
|
3404
|
+
def test_sanitize_preserves_clean_assistant_messages(self):
|
|
3405
|
+
"""Clean assistant messages are not modified by sanitization."""
|
|
3406
|
+
messages = [
|
|
3407
|
+
{"role": "assistant", "content": "I will read the file for you."},
|
|
3408
|
+
]
|
|
3409
|
+
sanitized = proxy._sanitize_assistant_messages_for_retry(messages)
|
|
3410
|
+
self.assertEqual(sanitized[0]["content"], "I will read the file for you.")
|
|
3411
|
+
|
|
3412
|
+
def test_sanitize_replaces_empty_content_with_placeholder(self):
|
|
3413
|
+
"""If stripping leaves empty content, a placeholder is used."""
|
|
3414
|
+
messages = [
|
|
3415
|
+
{"role": "assistant", "content": '<tool_call>{"name": "Bash", "arguments": {}}</tool_call>'},
|
|
3416
|
+
]
|
|
3417
|
+
sanitized = proxy._sanitize_assistant_messages_for_retry(messages)
|
|
3418
|
+
self.assertEqual(sanitized[0]["content"], "I will use the appropriate tool.")
|
|
3419
|
+
|
|
3420
|
+
def test_retry_body_uses_sanitized_messages(self):
|
|
3421
|
+
"""Retry body messages are sanitized before adding retry instruction."""
|
|
3422
|
+
openai_body = {
|
|
3423
|
+
"messages": [
|
|
3424
|
+
{"role": "system", "content": "sys"},
|
|
3425
|
+
{"role": "user", "content": "do it"},
|
|
3426
|
+
{"role": "assistant", "content": '<tool_call>{"name":"X","arguments":{}}</tool_call>'},
|
|
3427
|
+
],
|
|
3428
|
+
"tools": [{"type": "function", "function": {"name": "X", "parameters": {}}}],
|
|
3429
|
+
}
|
|
3430
|
+
anthropic_body = {"tools": [{"name": "X", "input_schema": {"type": "object"}}]}
|
|
3431
|
+
retry = proxy._build_malformed_retry_body(
|
|
3432
|
+
openai_body, anthropic_body, attempt=1, total_attempts=3,
|
|
3433
|
+
)
|
|
3434
|
+
# The assistant message should be sanitized
|
|
3435
|
+
assistant_msgs = [m for m in retry["messages"] if m.get("role") == "assistant"]
|
|
3436
|
+
for m in assistant_msgs:
|
|
3437
|
+
self.assertNotIn("<tool_call>", m.get("content", ""))
|
|
3438
|
+
|
|
3439
|
+
|
|
3380
3440
|
if __name__ == "__main__":
|
|
3381
3441
|
unittest.main()
|
|
3382
3442
|
|