@miller-tech/uap 1.84.1 → 1.85.0
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 +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +50 -0
- package/tools/agents/scripts/confidence_escalation.py +104 -0
- package/tools/agents/tests/test_confidence_escalation.py +56 -0
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -93,6 +93,11 @@ import sys
|
|
|
93
93
|
import time
|
|
94
94
|
import uuid
|
|
95
95
|
from collections import OrderedDict, defaultdict, deque
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
import confidence_escalation as _ce # serving-layer Confidence recipe
|
|
99
|
+
except Exception: # pragma: no cover - fail open if module missing
|
|
100
|
+
_ce = None
|
|
96
101
|
from dataclasses import dataclass, field
|
|
97
102
|
from pathlib import Path
|
|
98
103
|
|
|
@@ -2792,6 +2797,16 @@ def _should_use_guarded_non_stream(
|
|
|
2792
2797
|
if PROXY_MALFORMED_TOOL_STREAM_STRICT and has_tools:
|
|
2793
2798
|
return True
|
|
2794
2799
|
|
|
2800
|
+
# Confidence-escalation (vLLM "Confidence" recipe) needs the FULL answer
|
|
2801
|
+
# buffered to score it before deciding whether to escalate. Buffer single-
|
|
2802
|
+
# answer (no-tool) streaming turns when escalation is enabled. Default OFF.
|
|
2803
|
+
if not has_tools and _ce is not None:
|
|
2804
|
+
try:
|
|
2805
|
+
if _ce.Settings.from_env().enabled:
|
|
2806
|
+
return True
|
|
2807
|
+
except Exception:
|
|
2808
|
+
pass
|
|
2809
|
+
|
|
2795
2810
|
# A2: when stream-passthrough is enabled, do NOT buffer required-tool turns
|
|
2796
2811
|
# (native tool_choice='required' constrains them); force-non-stream and
|
|
2797
2812
|
# malformed-strict above still apply.
|
|
@@ -7988,6 +8003,40 @@ def openai_to_anthropic_response(
|
|
|
7988
8003
|
}
|
|
7989
8004
|
|
|
7990
8005
|
|
|
8006
|
+
async def _maybe_confidence_escalate(anthropic_resp, anthropic_body, client):
|
|
8007
|
+
"""Confidence recipe: if the cheap primary answer scores below threshold,
|
|
8008
|
+
escalate the same (non-tool) turn to the stronger backend and return its
|
|
8009
|
+
answer. Default OFF; fails open (returns the original) on any problem."""
|
|
8010
|
+
if _ce is None or not isinstance(anthropic_resp, dict):
|
|
8011
|
+
return anthropic_resp
|
|
8012
|
+
try:
|
|
8013
|
+
settings = _ce.Settings.from_env()
|
|
8014
|
+
text = _ce.extract_text(anthropic_resp)
|
|
8015
|
+
if not _ce.should_escalate(text, settings, _has_tool_definitions(anthropic_body)):
|
|
8016
|
+
return anthropic_resp
|
|
8017
|
+
payload = _ce.build_escalation_payload(anthropic_body, settings)
|
|
8018
|
+
url = settings.endpoint.rstrip("/") + "/v1/messages"
|
|
8019
|
+
headers = {
|
|
8020
|
+
"Content-Type": "application/json",
|
|
8021
|
+
"x-api-key": settings.api_key,
|
|
8022
|
+
"anthropic-version": "2023-06-01",
|
|
8023
|
+
}
|
|
8024
|
+
resp = await client.post(url, json=payload, headers=headers, timeout=120.0)
|
|
8025
|
+
if resp.status_code == 200:
|
|
8026
|
+
logger.warning(
|
|
8027
|
+
"CONFIDENCE ESCALATION: primary confidence below %.2f -> escalated to %s",
|
|
8028
|
+
settings.threshold, settings.model,
|
|
8029
|
+
)
|
|
8030
|
+
return resp.json()
|
|
8031
|
+
logger.warning(
|
|
8032
|
+
"CONFIDENCE ESCALATION: backend %s returned %d; keeping primary",
|
|
8033
|
+
settings.model, resp.status_code,
|
|
8034
|
+
)
|
|
8035
|
+
except Exception as exc:
|
|
8036
|
+
logger.warning("CONFIDENCE ESCALATION: failed (%s); keeping primary", exc)
|
|
8037
|
+
return anthropic_resp
|
|
8038
|
+
|
|
8039
|
+
|
|
7991
8040
|
async def _heartbeat_then_buffered(produce_coro, model: str):
|
|
7992
8041
|
"""SSE generator: keep-alive heartbeat wrapper for the guarded-non-stream path.
|
|
7993
8042
|
|
|
@@ -8953,6 +9002,7 @@ async def messages(request: Request):
|
|
|
8953
9002
|
"REQUIRED TOOL STREAM GUARDRAIL: served stream response via guarded non-stream path"
|
|
8954
9003
|
)
|
|
8955
9004
|
|
|
9005
|
+
anthropic_resp = await _maybe_confidence_escalate(anthropic_resp, body, client)
|
|
8956
9006
|
return anthropic_resp
|
|
8957
9007
|
|
|
8958
9008
|
if PROXY_STREAM_HEARTBEAT_SECS > 0:
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Confidence-escalation looper (vLLM Semantic Router "Confidence" recipe).
|
|
2
|
+
|
|
3
|
+
Try the cheap primary model first; if a confidence signal on its answer is below
|
|
4
|
+
threshold, escalate the SAME request to a stronger backend and return that
|
|
5
|
+
answer instead. Bounded (one escalation), default OFF, fails open. Scoped to
|
|
6
|
+
non-tool (single-answer) turns — escalating a mid-loop tool turn would need the
|
|
7
|
+
stronger model to share the whole tool/context state, which is out of scope for
|
|
8
|
+
this primitive.
|
|
9
|
+
|
|
10
|
+
The confidence signal is intentionally pluggable. This module ships a cheap
|
|
11
|
+
text heuristic; a stronger signal (token logprob margin, or UAP's real
|
|
12
|
+
execution/acceptance GATE-pass — the differentiator vs logprob) can replace
|
|
13
|
+
``text_confidence`` without touching the proxy hook.
|
|
14
|
+
|
|
15
|
+
All config is env-driven and OFF by default:
|
|
16
|
+
PROXY_CONFIDENCE_ESCALATE=on enable the looper
|
|
17
|
+
PROXY_CONFIDENCE_THRESHOLD=0.5 escalate when confidence < this
|
|
18
|
+
PROXY_ESCALATE_MODEL=<id> model id sent to the escalation backend
|
|
19
|
+
PROXY_ESCALATE_ENDPOINT=<url> Anthropic-compatible /v1/messages base
|
|
20
|
+
PROXY_ESCALATE_API_KEY=<key> x-api-key for the escalation backend
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Settings:
|
|
31
|
+
enabled: bool
|
|
32
|
+
threshold: float
|
|
33
|
+
model: str
|
|
34
|
+
endpoint: str
|
|
35
|
+
api_key: str
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_env(cls) -> "Settings":
|
|
39
|
+
on = os.environ.get("PROXY_CONFIDENCE_ESCALATE", "off").lower() not in {
|
|
40
|
+
"", "0", "off", "false", "no",
|
|
41
|
+
}
|
|
42
|
+
try:
|
|
43
|
+
thr = float(os.environ.get("PROXY_CONFIDENCE_THRESHOLD", "0.5"))
|
|
44
|
+
except ValueError:
|
|
45
|
+
thr = 0.5
|
|
46
|
+
return cls(
|
|
47
|
+
enabled=on,
|
|
48
|
+
threshold=thr,
|
|
49
|
+
model=os.environ.get("PROXY_ESCALATE_MODEL", ""),
|
|
50
|
+
endpoint=os.environ.get("PROXY_ESCALATE_ENDPOINT", ""),
|
|
51
|
+
api_key=os.environ.get("PROXY_ESCALATE_API_KEY", ""),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def backend_configured(self) -> bool:
|
|
55
|
+
return bool(self.model and self.endpoint)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_UNCERTAIN = re.compile(
|
|
59
|
+
r"\b(i'?m not sure|i am not sure|i don'?t know|i cannot|i can'?t (?:help|do|determine)|"
|
|
60
|
+
r"unable to|not certain|no idea|as an ai|i'?m sorry,? but)\b",
|
|
61
|
+
re.I,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def text_confidence(text: str) -> float:
|
|
66
|
+
"""Cheap heuristic confidence in [0,1]. Conservative: only clearly weak
|
|
67
|
+
answers (empty, refusal/uncertainty, trivially short) score low."""
|
|
68
|
+
t = (text or "").strip()
|
|
69
|
+
if not t:
|
|
70
|
+
return 0.0
|
|
71
|
+
if _UNCERTAIN.search(t):
|
|
72
|
+
return 0.2
|
|
73
|
+
if len(t) < 20:
|
|
74
|
+
return 0.3
|
|
75
|
+
return 0.9
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def extract_text(anthropic_resp: dict) -> str:
|
|
79
|
+
"""Concatenate text blocks of an Anthropic message response."""
|
|
80
|
+
parts = []
|
|
81
|
+
for blk in (anthropic_resp or {}).get("content", []) or []:
|
|
82
|
+
if isinstance(blk, dict) and blk.get("type") == "text":
|
|
83
|
+
parts.append(blk.get("text", ""))
|
|
84
|
+
return "".join(parts)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def should_escalate(text: str, settings: Settings, has_tools: bool) -> bool:
|
|
88
|
+
if not settings.enabled or not settings.backend_configured():
|
|
89
|
+
return False
|
|
90
|
+
if has_tools: # single-answer scope only
|
|
91
|
+
return False
|
|
92
|
+
return text_confidence(text) < settings.threshold
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_escalation_payload(anthropic_body: dict, settings: Settings) -> dict:
|
|
96
|
+
"""Re-issue the same conversation to the stronger model (no tools)."""
|
|
97
|
+
payload = {
|
|
98
|
+
"model": settings.model,
|
|
99
|
+
"max_tokens": anthropic_body.get("max_tokens", 4096),
|
|
100
|
+
"messages": anthropic_body.get("messages", []),
|
|
101
|
+
}
|
|
102
|
+
if anthropic_body.get("system"):
|
|
103
|
+
payload["system"] = anthropic_body["system"]
|
|
104
|
+
return payload
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Tests for the confidence-escalation looper (vLLM Confidence recipe)."""
|
|
2
|
+
import importlib.util
|
|
3
|
+
import unittest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
mod_path = Path(__file__).resolve().parents[3] / "tools" / "agents" / "scripts" / "confidence_escalation.py"
|
|
7
|
+
spec = importlib.util.spec_from_file_location("confidence_escalation", mod_path)
|
|
8
|
+
ce = importlib.util.module_from_spec(spec)
|
|
9
|
+
import sys as _sys; _sys.modules["confidence_escalation"]=ce; spec.loader.exec_module(ce)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def S(enabled=True, threshold=0.5, model="opus", endpoint="http://x/", api_key="k"):
|
|
13
|
+
return ce.Settings(enabled=enabled, threshold=threshold, model=model, endpoint=endpoint, api_key=api_key)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConfidenceTest(unittest.TestCase):
|
|
17
|
+
def test_text_confidence_levels(self):
|
|
18
|
+
self.assertEqual(ce.text_confidence(""), 0.0)
|
|
19
|
+
self.assertEqual(ce.text_confidence("I don't know"), 0.2)
|
|
20
|
+
self.assertEqual(ce.text_confidence("ok"), 0.3)
|
|
21
|
+
self.assertGreater(ce.text_confidence("Here is a complete, detailed answer to your question."), 0.5)
|
|
22
|
+
|
|
23
|
+
def test_extract_text(self):
|
|
24
|
+
resp = {"content": [{"type": "text", "text": "a"}, {"type": "tool_use"}, {"type": "text", "text": "b"}]}
|
|
25
|
+
self.assertEqual(ce.extract_text(resp), "ab")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ShouldEscalateTest(unittest.TestCase):
|
|
29
|
+
def test_low_confidence_escalates(self):
|
|
30
|
+
self.assertTrue(ce.should_escalate("I cannot help", S(), has_tools=False))
|
|
31
|
+
|
|
32
|
+
def test_high_confidence_does_not(self):
|
|
33
|
+
self.assertFalse(ce.should_escalate("A thorough and confident full answer here.", S(), has_tools=False))
|
|
34
|
+
|
|
35
|
+
def test_disabled_never(self):
|
|
36
|
+
self.assertFalse(ce.should_escalate("", S(enabled=False), has_tools=False))
|
|
37
|
+
|
|
38
|
+
def test_no_backend_never(self):
|
|
39
|
+
self.assertFalse(ce.should_escalate("", S(endpoint=""), has_tools=False))
|
|
40
|
+
|
|
41
|
+
def test_tool_turn_never(self):
|
|
42
|
+
self.assertFalse(ce.should_escalate("", S(), has_tools=True))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PayloadTest(unittest.TestCase):
|
|
46
|
+
def test_payload_uses_escalation_model_and_messages(self):
|
|
47
|
+
body = {"model": "qwen", "max_tokens": 100, "system": "sys", "messages": [{"role": "user", "content": "hi"}]}
|
|
48
|
+
p = ce.build_escalation_payload(body, S(model="opus"))
|
|
49
|
+
self.assertEqual(p["model"], "opus")
|
|
50
|
+
self.assertEqual(p["messages"], body["messages"])
|
|
51
|
+
self.assertEqual(p["system"], "sys")
|
|
52
|
+
self.assertNotIn("tools", p)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
unittest.main()
|